Complete guided Scheduling interface patterns

This commit is contained in:
2026-08-03 10:36:14 +02:00
parent 3d8272b28e
commit c17cbdae63
9 changed files with 334 additions and 52 deletions
@@ -1,11 +1,13 @@
import { useCallback } from "react";
import { useCallback, type ReactNode } from "react";
import { CalendarClock } from "lucide-react";
import { Link } from "react-router";
import {
DashboardWidgetList,
DismissibleAlert,
DocumentationHelpLink,
LoadingFrame,
StatusBadge,
formatDateTime,
useDashboardWidgetData,
type ApiSettings,
type DashboardWidgetConfiguration
@@ -15,6 +17,17 @@ import {
type SchedulingRequest
} from "../../api/scheduling";
const I18N = {
candidateSlots: "i18n:govoplan-scheduling.candidate_slots.c414946b",
deadline: "i18n:govoplan-scheduling.response_deadline.7fd9e3aa",
draft: "i18n:govoplan-scheduling.draft.23d33e22",
empty: "i18n:govoplan-scheduling.no_scheduling_request_selected.ac940664",
loading: "i18n:govoplan-scheduling.loading_scheduling_requests.f42be95d",
open: "i18n:govoplan-scheduling.open.cf9b7706",
openScheduling: "i18n:govoplan-scheduling.open_in_scheduling.48df1541",
responded: "i18n:govoplan-scheduling.responded.4f218211"
} as const;
export default function SchedulingRequestsWidget({
settings,
refreshKey,
@@ -43,14 +56,14 @@ export default function SchedulingRequestsWidget({
);
return (
<LoadingFrame loading={loading} label="Loading scheduling requests">
<LoadingFrame loading={loading} label={I18N.loading}>
{error && (
<DismissibleAlert tone="warning" resetKey={error}>
{error}
</DismissibleAlert>
)}
<DashboardWidgetList
emptyText="No scheduling requests are awaiting responses."
emptyText={I18N.empty}
items={(requests ?? []).map((request) => ({
id: request.id,
title: request.title,
@@ -60,15 +73,20 @@ export default function SchedulingRequestsWidget({
trailing: (
<StatusBadge
status={request.status}
label={request.status === "collecting" ? "Open" : "Draft"}
label={request.status === "collecting" ? I18N.open : I18N.draft}
/>
),
to: "/scheduling"
to: `/scheduling?request_id=${encodeURIComponent(request.id)}`
}))}
/>
<div className="dashboard-contribution-footer">
<DocumentationHelpLink
reference={{
topicId: "scheduling.find-and-decide-meeting-time",
documentationType: "user"
}} />
<Link className="btn btn-secondary" to="/scheduling">
Open scheduling
{I18N.openScheduling}
</Link>
</div>
</LoadingFrame>
@@ -94,17 +112,17 @@ function compareRequests(
);
}
function responseLabel(request: SchedulingRequest): string {
function responseLabel(request: SchedulingRequest): ReactNode {
const responded = request.participant_aggregate.status_counts.responded ?? 0;
return `${responded} of ${request.participant_aggregate.total} responded`;
return <>{responded}/{request.participant_aggregate.total} {I18N.responded}</>;
}
function deadlineLabel(request: SchedulingRequest): string {
if (!request.deadline_at) return `${request.slots.length} options`;
return `Due ${new Intl.DateTimeFormat(undefined, {
function deadlineLabel(request: SchedulingRequest): ReactNode {
if (!request.deadline_at) return <>{request.slots.length} {I18N.candidateSlots}</>;
return <>{I18N.deadline}: {formatDateTime(request.deadline_at, {
day: "2-digit",
month: "short"
}).format(new Date(request.deadline_at))}`;
})}</>;
}
function numberSetting(