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;
|
||||
|
||||
Reference in New Issue
Block a user