Apply guided scheduling interface patterns
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
XCircle
|
||||
} from "lucide-react";
|
||||
import {
|
||||
ActionBlockerHint,
|
||||
Button,
|
||||
Card,
|
||||
ConfirmDialog,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
DataGridRowActions,
|
||||
DateTimeField,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
FormField,
|
||||
MetricCard,
|
||||
IconButton,
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
TableActionGroup,
|
||||
ToggleSwitch,
|
||||
StatusBadge,
|
||||
StageRail,
|
||||
hasScope,
|
||||
i18nMessage,
|
||||
isApiError,
|
||||
@@ -84,12 +87,14 @@ import {
|
||||
participantDraftsFromPicker,
|
||||
participantPayload,
|
||||
schedulingInvitationActionBlocks,
|
||||
schedulingLifecycleStages,
|
||||
schedulingPublicInvitationUrl,
|
||||
schedulingRelevantTimestamp,
|
||||
schedulingRequestIsOwned,
|
||||
schedulingSortPhase,
|
||||
type SchedulingActor,
|
||||
type SchedulingInvitationActionBlock,
|
||||
type SchedulingLifecycleStageState,
|
||||
type SchedulingParticipantDraft,
|
||||
type SchedulingRequestGroups
|
||||
} from "./schedulingViewModel";
|
||||
@@ -130,6 +135,7 @@ const I18N = {
|
||||
finalEventLabel: "i18n:govoplan-scheduling.create_calendar_event.0b87cfcf",
|
||||
calendarIntegration: "i18n:govoplan-scheduling.calendar_integration.181ad18b",
|
||||
calendarUnavailable: "i18n:govoplan-scheduling.calendar_integration_requires_the_calendar_module_plus_c.f892cb1e",
|
||||
calendarRequiredAction: "i18n:govoplan-scheduling.enable_calendar_and_grant_calendar_availability_and_event_access.f1a20106",
|
||||
candidateAvailability: "i18n:govoplan-scheduling.candidate_availability.9541c4b5",
|
||||
candidateSlots: "i18n:govoplan-scheduling.candidate_slots.c414946b",
|
||||
cancellationNoticeExpired: "i18n:govoplan-scheduling.the_cancellation_notice_has_expired_a_new_link_cannot_be_issued.9c6ccc7c",
|
||||
@@ -142,6 +148,7 @@ const I18N = {
|
||||
description: "i18n:govoplan-scheduling.description.55f8ebc8",
|
||||
determined: "i18n:govoplan-scheduling.determined.9f23293d",
|
||||
decideUnavailable: "i18n:govoplan-scheduling.a_slot_can_be_selected_after_the_request_is_closed.f91ec02d",
|
||||
decision: "i18n:govoplan-scheduling.decision.7f59a1f1",
|
||||
discard: "i18n:govoplan-scheduling.discard.36fff63c",
|
||||
discardConfirm: "i18n:govoplan-scheduling.discard_this_unsaved_scheduling_request.4a956be2",
|
||||
edit: "i18n:govoplan-scheduling.edit_scheduling_request.7e749c19",
|
||||
@@ -215,6 +222,7 @@ const I18N = {
|
||||
revokeLink: "i18n:govoplan-scheduling.revoke_link.da371ee1",
|
||||
requestFailed: "i18n:govoplan-scheduling.request_failed.9fcda32c",
|
||||
publicPolicyUnavailable: "i18n:govoplan-scheduling.guest_links_are_not_issued_while_the_configured_participation.0794ebf0",
|
||||
publicPolicyRequiredAction: "i18n:govoplan-scheduling.enable_enforceable_public_participation_controls_or_keep_participation_signed_in.f1a20105",
|
||||
policyLocked: "i18n:govoplan-scheduling.participation_controls_are_locked_after_invitation_links_are_issued.66f5a740",
|
||||
requests: "i18n:govoplan-scheduling.scheduling_requests.b3c12f4d",
|
||||
requiresAvailabilityRead: "i18n:govoplan-scheduling.requires_calendar_availability_read_access.b48ed91b",
|
||||
@@ -236,6 +244,11 @@ const I18N = {
|
||||
statusLabel: "i18n:govoplan-scheduling.status.bae7d5be",
|
||||
title: "i18n:govoplan-scheduling.title.768e0c1c",
|
||||
titleRequired: "i18n:govoplan-scheduling.scheduling_request_title_is_required.a27338be",
|
||||
requiredAction: "i18n:govoplan-scheduling.required_action.f1a20101",
|
||||
responsibleActor: "i18n:govoplan-scheduling.who_can_fix_it.f1a20102",
|
||||
resolutionTarget: "i18n:govoplan-scheduling.where_to_go.f1a20103",
|
||||
systemOrTenantAdministrator: "i18n:govoplan-scheduling.system_or_tenant_administrator.f1a20104",
|
||||
administration: "i18n:govoplan-core.admin.4e7afebc",
|
||||
unchecked: "i18n:govoplan-scheduling.unchecked.1b927dec",
|
||||
unavailable: "i18n:govoplan-scheduling.unavailable.2c9c1f79",
|
||||
updateResponse: "i18n:govoplan-scheduling.update_response.346233cf",
|
||||
@@ -935,7 +948,25 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
if (!checked) setCalendarId("");
|
||||
setDraftDirty(true);
|
||||
}} />
|
||||
{!calendarIntegrationAvailable ? <p className="scheduling-capability-note">{I18N.calendarUnavailable}</p> : null}
|
||||
{!calendarIntegrationAvailable ? (
|
||||
<ActionBlockerHint
|
||||
tone="info"
|
||||
reason={{
|
||||
summary: I18N.calendarUnavailable,
|
||||
requiredAction: I18N.calendarRequiredAction,
|
||||
actor: I18N.systemOrTenantAdministrator,
|
||||
target: I18N.administration
|
||||
}}
|
||||
labels={{
|
||||
requiredAction: I18N.requiredAction,
|
||||
actor: I18N.responsibleActor,
|
||||
target: I18N.resolutionTarget
|
||||
}}
|
||||
documentation={{
|
||||
topicId: "scheduling.calendar-coordination",
|
||||
documentationType: "admin"
|
||||
}} />
|
||||
) : null}
|
||||
{calendarEnabled && CalendarPicker ? (
|
||||
<CalendarPicker
|
||||
settings={settings}
|
||||
@@ -1030,10 +1061,36 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
</div>
|
||||
) : selected ? (
|
||||
<div className="scheduling-detail">
|
||||
<StageRail
|
||||
className="scheduling-lifecycle-rail"
|
||||
ariaLabel={I18N.requests}
|
||||
items={schedulingLifecycleStages(selected.status).map((stage) => ({
|
||||
id: stage.id,
|
||||
label: stage.id === "prepare"
|
||||
? I18N.basicInformation
|
||||
: stage.id === "participate"
|
||||
? I18N.participation
|
||||
: I18N.decision,
|
||||
icon: stage.id === "prepare"
|
||||
? <Pencil aria-hidden="true" size={15} />
|
||||
: stage.id === "participate"
|
||||
? <Send aria-hidden="true" size={15} />
|
||||
: <Check aria-hidden="true" size={15} />,
|
||||
tone: lifecycleStageTone(stage.state),
|
||||
current: stage.current,
|
||||
locked: stage.locked,
|
||||
lockedLabel: I18N.unavailable,
|
||||
statusLabel: stage.current ? requestStatusLabel(selected, actor) : undefined
|
||||
}))} />
|
||||
<Card
|
||||
title={selected.title}
|
||||
actions={(
|
||||
<div className="scheduling-actions">
|
||||
<DocumentationHelpLink
|
||||
reference={{
|
||||
topicId: "scheduling.find-and-decide-meeting-time",
|
||||
documentationType: "user"
|
||||
}} />
|
||||
{canEditSelected ? (
|
||||
<Button type="button" onClick={() => beginEdit(selected)} disabled={saving}>
|
||||
<Pencil aria-hidden="true" size={16} /> {I18N.edit}
|
||||
@@ -1070,9 +1127,23 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
|
||||
) : null}
|
||||
|
||||
{selected.public_participation_policy_enforcement_available === false ? (
|
||||
<DismissibleAlert tone="warning" dismissible={false} compact>
|
||||
{selected.public_participation_policy_enforcement_reason || I18N.publicPolicyUnavailable}
|
||||
</DismissibleAlert>
|
||||
<ActionBlockerHint
|
||||
reason={{
|
||||
summary: selected.public_participation_policy_enforcement_reason || I18N.publicPolicyUnavailable,
|
||||
details: I18N.publicPolicyUnavailable,
|
||||
requiredAction: I18N.publicPolicyRequiredAction,
|
||||
actor: I18N.systemOrTenantAdministrator,
|
||||
target: I18N.administration
|
||||
}}
|
||||
labels={{
|
||||
requiredAction: I18N.requiredAction,
|
||||
actor: I18N.responsibleActor,
|
||||
target: I18N.resolutionTarget
|
||||
}}
|
||||
documentation={{
|
||||
topicId: "scheduling.participation-governance",
|
||||
documentationType: "admin"
|
||||
}} />
|
||||
) : null}
|
||||
|
||||
{selectedParticipant && selected.status === "collecting" ? (
|
||||
@@ -1957,6 +2028,15 @@ function firstRequest(groups: SchedulingRequestGroups, includeManaged: boolean):
|
||||
return groups.owned[0] ?? groups.invited[0] ?? (includeManaged ? groups.other[0] : null) ?? null;
|
||||
}
|
||||
|
||||
function lifecycleStageTone(
|
||||
state: SchedulingLifecycleStageState
|
||||
): "success" | "active" | "warning" | "neutral" {
|
||||
if (state === "complete") return "success";
|
||||
if (state === "current") return "active";
|
||||
if (state === "stopped") return "warning";
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
function requestStatusLabel(request: SchedulingRequest, actor: SchedulingActor): string {
|
||||
const phase = schedulingSortPhase(request, actor);
|
||||
if (phase === "past") return I18N.past;
|
||||
|
||||
@@ -35,6 +35,21 @@ export type SchedulingRequestGroups = {
|
||||
other: SchedulingRequest[];
|
||||
};
|
||||
|
||||
export type SchedulingLifecycleStageId = "prepare" | "participate" | "decide";
|
||||
|
||||
export type SchedulingLifecycleStageState =
|
||||
| "complete"
|
||||
| "current"
|
||||
| "locked"
|
||||
| "stopped";
|
||||
|
||||
export type SchedulingLifecycleStage = {
|
||||
id: SchedulingLifecycleStageId;
|
||||
state: SchedulingLifecycleStageState;
|
||||
current: boolean;
|
||||
locked: boolean;
|
||||
};
|
||||
|
||||
export type SchedulingSortPhase =
|
||||
| "unanswered"
|
||||
| "answered"
|
||||
@@ -282,6 +297,37 @@ export function schedulingRequestIsPast(
|
||||
return slotEnds.every((value) => Number.isFinite(value) && value < now.getTime());
|
||||
}
|
||||
|
||||
export function schedulingLifecycleStages(
|
||||
status: SchedulingRequest["status"]
|
||||
): SchedulingLifecycleStage[] {
|
||||
const currentIndex = status === "draft"
|
||||
? 0
|
||||
: status === "collecting" || status === "cancelled"
|
||||
? 1
|
||||
: 2;
|
||||
const completedThrough = status === "draft"
|
||||
? -1
|
||||
: status === "collecting" || status === "cancelled"
|
||||
? 0
|
||||
: status === "closed"
|
||||
? 1
|
||||
: 2;
|
||||
const stopped = status === "cancelled";
|
||||
|
||||
return (["prepare", "participate", "decide"] as const).map((id, index) => {
|
||||
const current = index === currentIndex;
|
||||
const locked = index > completedThrough + 1;
|
||||
const state: SchedulingLifecycleStageState = stopped && current
|
||||
? "stopped"
|
||||
: index <= completedThrough
|
||||
? "complete"
|
||||
: current
|
||||
? "current"
|
||||
: "locked";
|
||||
return { id, state, current, locked };
|
||||
});
|
||||
}
|
||||
|
||||
export function schedulingInvitationActionBlocks(
|
||||
request: SchedulingRequest,
|
||||
participant: SchedulingParticipant,
|
||||
|
||||
@@ -78,6 +78,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.configured_calendar.e2e8ebd5": "Configured calendar",
|
||||
"i18n:govoplan-scheduling.calendar_integration.181ad18b": "Calendar integration",
|
||||
"i18n:govoplan-scheduling.calendar_integration_requires_the_calendar_module_plus_c.f892cb1e": "Calendar integration requires the Calendar module plus calendar-read, availability-read, and event-write access.",
|
||||
"i18n:govoplan-scheduling.enable_calendar_and_grant_calendar_availability_and_event_access.f1a20106": "Enable Calendar and grant calendar, availability, and event access.",
|
||||
"i18n:govoplan-scheduling.candidate_availability.9541c4b5": "Candidate availability",
|
||||
"i18n:govoplan-scheduling.candidate_slots.c414946b": "Candidate slots",
|
||||
"i18n:govoplan-scheduling.check_free_busy.e9700e00": "Check free/busy",
|
||||
@@ -97,6 +98,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.discard.36fff63c": "Discard",
|
||||
"i18n:govoplan-scheduling.discard_this_unsaved_scheduling_request.4a956be2": "Discard this unsaved scheduling request?",
|
||||
"i18n:govoplan-scheduling.draft.23d33e22": "Draft",
|
||||
"i18n:govoplan-scheduling.enable_enforceable_public_participation_controls_or_keep_participation_signed_in.f1a20105": "Enable enforceable public participation controls, or keep participation signed in.",
|
||||
"i18n:govoplan-scheduling.end.a2bb9d34": "End",
|
||||
"i18n:govoplan-scheduling.error.7f2f6a15": "Error",
|
||||
"i18n:govoplan-scheduling.every_candidate_slot_must_end_after_it_starts.47836010": "Every candidate slot must end after it starts.",
|
||||
@@ -133,6 +135,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.pending.96f608c1": "Pending",
|
||||
"i18n:govoplan-scheduling.queued.6a599877": "Queued",
|
||||
"i18n:govoplan-scheduling.refresh_requests.0a3ed7a1": "Refresh requests",
|
||||
"i18n:govoplan-scheduling.required_action.f1a20101": "Required action",
|
||||
"i18n:govoplan-scheduling.reminder_creates_a_notification_job_for_every_active_par.7ec68797": "Reminder creates a notification job for every active participant.",
|
||||
"i18n:govoplan-scheduling.remove.e963907d": "Remove",
|
||||
"i18n:govoplan-scheduling.remove_participant_value.e55f2b70": "Remove participant {value0}",
|
||||
@@ -157,6 +160,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.sent.35f49dcf": "Sent",
|
||||
"i18n:govoplan-scheduling.skipped.5a000ad7": "Skipped",
|
||||
"i18n:govoplan-scheduling.start.952f3754": "Start",
|
||||
"i18n:govoplan-scheduling.system_or_tenant_administrator.f1a20104": "System or tenant administrator",
|
||||
"i18n:govoplan-scheduling.status.bae7d5be": "Status",
|
||||
"i18n:govoplan-scheduling.submit_response.a5f0c053": "Submit response",
|
||||
"i18n:govoplan-scheduling.tentative_holds_create_one_provisional_calendar_event_pe.ff3f1884": "Tentative holds create one provisional calendar event per candidate slot.",
|
||||
@@ -170,6 +174,8 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.value_participants.e776b092": "{value0} participants",
|
||||
"i18n:govoplan-scheduling.value_responses.ba17af9a": "{value0} responses",
|
||||
"i18n:govoplan-scheduling.what_do_these_actions_do.9a9aee0e": "What do these actions do?",
|
||||
"i18n:govoplan-scheduling.where_to_go.f1a20103": "Where to go",
|
||||
"i18n:govoplan-scheduling.who_can_fix_it.f1a20102": "Who can fix it",
|
||||
"i18n:govoplan-scheduling.you_can_respond_here_or_use_the_invitation_link_you_rece.1a25fd53": "You can respond here or use the invitation link you received.",
|
||||
"i18n:govoplan-scheduling.your_response_has_been_recorded.b855088d": "Your response has been recorded."
|
||||
},
|
||||
@@ -252,6 +258,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.configured_calendar.e2e8ebd5": "Ausgewählter Kalender",
|
||||
"i18n:govoplan-scheduling.calendar_integration.181ad18b": "Kalenderintegration",
|
||||
"i18n:govoplan-scheduling.calendar_integration_requires_the_calendar_module_plus_c.f892cb1e": "Die Kalenderintegration benötigt das Kalendermodul sowie Leserechte für Kalender und Verfügbarkeiten und Schreibrechte für Termine.",
|
||||
"i18n:govoplan-scheduling.enable_calendar_and_grant_calendar_availability_and_event_access.f1a20106": "Aktivieren Sie Kalender und vergeben Sie Rechte für Kalender, Verfügbarkeiten und Termine.",
|
||||
"i18n:govoplan-scheduling.candidate_availability.9541c4b5": "Verfügbarkeit zu den Vorschlägen",
|
||||
"i18n:govoplan-scheduling.candidate_slots.c414946b": "Terminvorschläge",
|
||||
"i18n:govoplan-scheduling.check_free_busy.e9700e00": "Frei/Belegt prüfen",
|
||||
@@ -271,6 +278,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.discard.36fff63c": "Verwerfen",
|
||||
"i18n:govoplan-scheduling.discard_this_unsaved_scheduling_request.4a956be2": "Diese ungespeicherte Terminanfrage verwerfen?",
|
||||
"i18n:govoplan-scheduling.draft.23d33e22": "Entwurf",
|
||||
"i18n:govoplan-scheduling.enable_enforceable_public_participation_controls_or_keep_participation_signed_in.f1a20105": "Aktivieren Sie durchsetzbare Regeln für die öffentliche Teilnahme oder beschränken Sie die Teilnahme auf angemeldete Personen.",
|
||||
"i18n:govoplan-scheduling.end.a2bb9d34": "Ende",
|
||||
"i18n:govoplan-scheduling.error.7f2f6a15": "Fehler",
|
||||
"i18n:govoplan-scheduling.every_candidate_slot_must_end_after_it_starts.47836010": "Jeder Terminvorschlag muss nach seinem Beginn enden.",
|
||||
@@ -307,6 +315,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.pending.96f608c1": "Ausstehend",
|
||||
"i18n:govoplan-scheduling.queued.6a599877": "Eingereiht",
|
||||
"i18n:govoplan-scheduling.refresh_requests.0a3ed7a1": "Anfragen aktualisieren",
|
||||
"i18n:govoplan-scheduling.required_action.f1a20101": "Erforderliche Maßnahme",
|
||||
"i18n:govoplan-scheduling.reminder_creates_a_notification_job_for_every_active_par.7ec68797": "Erinnern erstellt für jede aktive teilnehmende Person einen Benachrichtigungsauftrag.",
|
||||
"i18n:govoplan-scheduling.remove.e963907d": "Entfernen",
|
||||
"i18n:govoplan-scheduling.remove_participant_value.e55f2b70": "Teilnehmende Person {value0} entfernen",
|
||||
@@ -331,6 +340,7 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.sent.35f49dcf": "Gesendet",
|
||||
"i18n:govoplan-scheduling.skipped.5a000ad7": "Übersprungen",
|
||||
"i18n:govoplan-scheduling.start.952f3754": "Beginn",
|
||||
"i18n:govoplan-scheduling.system_or_tenant_administrator.f1a20104": "System- oder Mandantenadministration",
|
||||
"i18n:govoplan-scheduling.status.bae7d5be": "Status",
|
||||
"i18n:govoplan-scheduling.submit_response.a5f0c053": "Antwort senden",
|
||||
"i18n:govoplan-scheduling.tentative_holds_create_one_provisional_calendar_event_pe.ff3f1884": "Vorläufige Reservierungen erstellen je Terminvorschlag einen provisorischen Kalendereintrag.",
|
||||
@@ -344,6 +354,8 @@ export const generatedTranslations = {
|
||||
"i18n:govoplan-scheduling.value_participants.e776b092": "{value0} Teilnehmende",
|
||||
"i18n:govoplan-scheduling.value_responses.ba17af9a": "{value0} Antworten",
|
||||
"i18n:govoplan-scheduling.what_do_these_actions_do.9a9aee0e": "Was bewirken diese Aktionen?",
|
||||
"i18n:govoplan-scheduling.where_to_go.f1a20103": "Ziel",
|
||||
"i18n:govoplan-scheduling.who_can_fix_it.f1a20102": "Zuständig",
|
||||
"i18n:govoplan-scheduling.you_can_respond_here_or_use_the_invitation_link_you_rece.1a25fd53": "Sie können hier oder über den erhaltenen Einladungslink antworten.",
|
||||
"i18n:govoplan-scheduling.your_response_has_been_recorded.b855088d": "Ihre Antwort wurde gespeichert."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user