|
|
@@ -1,6 +1,6 @@
|
|
|
|
import { AlarmClock, Check, Plus, RefreshCw, X } from "lucide-react";
|
|
|
|
import { AlarmClock, Check, Plus, RefreshCw, X } from "lucide-react";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
import { ActionBlockerHint, Button, ConfirmDialog, Dialog, DocumentationHelpLink, DismissibleAlert, FormField, IconButton, LoadingIndicator, PageScrollViewport, StatusBadge, hasScope, usePlatformLanguage, useUnsavedChanges, useUnsavedDraftGuard, type PlatformRouteContext } from "@govoplan/core-webui";
|
|
|
|
import { ActionBlockerHint, Button, ConfirmDialog, Dialog, DocumentationHelpLink, DismissibleAlert, FormField, IconButton, LoadingIndicator, PageScrollViewport, StatusBadge, WorkspaceLayout, hasScope, usePlatformLanguage, useUnsavedChanges, useUnsavedDraftGuard, type PlatformRouteContext } from "@govoplan/core-webui";
|
|
|
|
import { approvalHistory, decideApproval, escalateApproval, getApproval, listApprovals, type ApprovalEvent, type ApprovalRequest } from "../../api/approvals";
|
|
|
|
import { approvalHistory, decideApproval, escalateApproval, getApproval, listApprovals, type ApprovalEvent, type ApprovalRequest } from "../../api/approvals";
|
|
|
|
import ApprovalRequestDialog from "./ApprovalRequestDialog";
|
|
|
|
import ApprovalRequestDialog from "./ApprovalRequestDialog";
|
|
|
|
import { APPROVALS_DOCUMENTATION, APPROVALS_FIELD_DOCUMENTATION, APPROVALS_I18N } from "./interfacePatterns";
|
|
|
|
import { APPROVALS_DOCUMENTATION, APPROVALS_FIELD_DOCUMENTATION, APPROVALS_I18N } from "./interfacePatterns";
|
|
|
@@ -54,12 +54,24 @@ export default function ApprovalsPage({ settings, auth }: PlatformRouteContext)
|
|
|
|
await load(undefined, id);
|
|
|
|
await load(undefined, id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return <main className="approvals-page"><div className="approvals-shell">
|
|
|
|
return <main className="approvals-page"><WorkspaceLayout
|
|
|
|
<aside className="approvals-list-panel">
|
|
|
|
variant="split"
|
|
|
|
|
|
|
|
primarySize="compact"
|
|
|
|
|
|
|
|
primaryScrollable={false}
|
|
|
|
|
|
|
|
contentScrollable={false}
|
|
|
|
|
|
|
|
className="approvals-shell"
|
|
|
|
|
|
|
|
primaryClassName="approvals-list-panel"
|
|
|
|
|
|
|
|
contentClassName="approvals-workspace"
|
|
|
|
|
|
|
|
primaryLabel="Approval requests"
|
|
|
|
|
|
|
|
contentLabel="Approval request details"
|
|
|
|
|
|
|
|
interfaceId="approvals.workspace"
|
|
|
|
|
|
|
|
helpContextId="approvals.workspace"
|
|
|
|
|
|
|
|
helpModuleId="approvals"
|
|
|
|
|
|
|
|
primary={<>
|
|
|
|
<div className="approvals-toolbar"><IconButton label="Refresh approvals" icon={<RefreshCw size={16} />} disabled={loading || busy} disabledReason={loading ? APPROVALS_I18N.loading : busy ? APPROVALS_I18N.busy : undefined} onClick={() => void load()} /><Button variant="primary" disabled={!canWrite} disabledReason={!canWrite ? APPROVALS_I18N.writeReason : undefined} onClick={() => setCreating(true)}><Plus size={16} aria-hidden="true" />New request</Button><DocumentationHelpLink reference={APPROVALS_DOCUMENTATION} /></div>
|
|
|
|
<div className="approvals-toolbar"><IconButton label="Refresh approvals" icon={<RefreshCw size={16} />} disabled={loading || busy} disabledReason={loading ? APPROVALS_I18N.loading : busy ? APPROVALS_I18N.busy : undefined} onClick={() => void load()} /><Button variant="primary" disabled={!canWrite} disabledReason={!canWrite ? APPROVALS_I18N.writeReason : undefined} onClick={() => setCreating(true)}><Plus size={16} aria-hidden="true" />New request</Button><DocumentationHelpLink reference={APPROVALS_DOCUMENTATION} /></div>
|
|
|
|
<PageScrollViewport className="approvals-list-viewport">{loading && <LoadingIndicator label="Loading approvals" />}<div className="approvals-list">{items.map((item) => <button type="button" key={item.id} className={item.id === selectedId ? "is-selected" : ""} onClick={() => setSelectedId(item.id)}><span><strong>{item.title}</strong><small>{item.subject_module} / {item.subject_type}</small></span><StatusBadge status={tone(item.state)} label={humanize(item.state)} /></button>)}</div>{!loading && items.length === 0 && <div className="approvals-empty">No approval requests</div>}</PageScrollViewport>
|
|
|
|
<PageScrollViewport className="approvals-list-viewport">{loading && <LoadingIndicator label="Loading approvals" />}<div className="approvals-list">{items.map((item) => <button type="button" key={item.id} className={item.id === selectedId ? "is-selected" : ""} onClick={() => setSelectedId(item.id)}><span><strong>{item.title}</strong><small>{item.subject_module} / {item.subject_type}</small></span><StatusBadge status={tone(item.state)} label={humanize(item.state)} /></button>)}</div>{!loading && items.length === 0 && <div className="approvals-empty">No approval requests</div>}</PageScrollViewport>
|
|
|
|
</aside>
|
|
|
|
</>}
|
|
|
|
<section className="approvals-workspace">
|
|
|
|
>
|
|
|
|
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
|
|
|
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
|
|
|
{!canWrite && <ActionBlockerHint tone="info" reason={{ summary: "No Approval creation permission", details: APPROVALS_I18N.writeReason, requiredAction: APPROVALS_I18N.permissionAction, actor: APPROVALS_I18N.permissionActor, target: APPROVALS_I18N.permissionDestination }} labels={{ requiredAction: APPROVALS_I18N.requiredAction, actor: APPROVALS_I18N.actor, target: APPROVALS_I18N.destination }} documentation={APPROVALS_DOCUMENTATION} />}
|
|
|
|
{!canWrite && <ActionBlockerHint tone="info" reason={{ summary: "No Approval creation permission", details: APPROVALS_I18N.writeReason, requiredAction: APPROVALS_I18N.permissionAction, actor: APPROVALS_I18N.permissionActor, target: APPROVALS_I18N.permissionDestination }} labels={{ requiredAction: APPROVALS_I18N.requiredAction, actor: APPROVALS_I18N.actor, target: APPROVALS_I18N.destination }} documentation={APPROVALS_DOCUMENTATION} />}
|
|
|
|
{selected && <PageScrollViewport className="approvals-detail-viewport"><div className="approvals-detail">
|
|
|
|
{selected && <PageScrollViewport className="approvals-detail-viewport"><div className="approvals-detail">
|
|
|
@@ -70,8 +82,7 @@ export default function ApprovalsPage({ settings, auth }: PlatformRouteContext)
|
|
|
|
<section><h3>History</h3><div className="approval-history">{history.map((event) => <div key={event.sequence}><span>{event.sequence}</span><strong>{humanize(event.event_type)}</strong><time>{new Date(event.recorded_at).toLocaleString(language)}</time></div>)}</div></section>
|
|
|
|
<section><h3>History</h3><div className="approval-history">{history.map((event) => <div key={event.sequence}><span>{event.sequence}</span><strong>{humanize(event.event_type)}</strong><time>{new Date(event.recorded_at).toLocaleString(language)}</time></div>)}</div></section>
|
|
|
|
</div></PageScrollViewport>}
|
|
|
|
</div></PageScrollViewport>}
|
|
|
|
{!selected && !loading && <div className="approvals-empty">Select or create an approval request.</div>}
|
|
|
|
{!selected && !loading && <div className="approvals-empty">Select or create an approval request.</div>}
|
|
|
|
</section>
|
|
|
|
</WorkspaceLayout>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{creating && <ApprovalRequestDialog settings={settings} onClose={() => setCreating(false)} onSaved={(item) => { setCreating(false); setSelectedId(item.id); void load(undefined, item.id); }} />}
|
|
|
|
{creating && <ApprovalRequestDialog settings={settings} onClose={() => setCreating(false)} onSaved={(item) => { setCreating(false); setSelectedId(item.id); void load(undefined, item.id); }} />}
|
|
|
|
{selected && decision && <DecisionDialog outcome={decision} busy={busy} signatureRequired={Boolean(selected.steps[selected.current_step_index]?.signature_required)} onClose={() => setDecision(null)} onConfirm={async (reason, signatureId) => { setBusy(true); setError(""); try { await decideApproval(settings, selected, decision, reason, signatureId ? { owner_module: "signatures", object_id: signatureId } : undefined); setDecision(null); await reload(selected.id); return true; } catch (failure) { setError(text(failure, "The Approval decision could not be recorded.")); return false; } finally { setBusy(false); } }} />}
|
|
|
|
{selected && decision && <DecisionDialog outcome={decision} busy={busy} signatureRequired={Boolean(selected.steps[selected.current_step_index]?.signature_required)} onClose={() => setDecision(null)} onConfirm={async (reason, signatureId) => { setBusy(true); setError(""); try { await decideApproval(settings, selected, decision, reason, signatureId ? { owner_module: "signatures", object_id: signatureId } : undefined); setDecision(null); await reload(selected.id); return true; } catch (failure) { setError(text(failure, "The Approval decision could not be recorded.")); return false; } finally { setBusy(false); } }} />}
|
|
|
|
{selected && <ConfirmDialog open={escalating} title="Escalate approval step" message={`Escalate ${currentStep?.label ?? "the current step"}? This records an explicit lifecycle transition and lets the configured escalation workflow react.`} confirmLabel="Escalate step" busy={busy} onCancel={() => setEscalating(false)} onConfirm={() => { setBusy(true); setError(""); void escalateApproval(settings, selected).then(() => { setEscalating(false); return reload(selected.id); }).catch((failure) => setError(text(failure, "The Approval step could not be escalated."))).finally(() => setBusy(false)); }} />}
|
|
|
|
{selected && <ConfirmDialog open={escalating} title="Escalate approval step" message={`Escalate ${currentStep?.label ?? "the current step"}? This records an explicit lifecycle transition and lets the configured escalation workflow react.`} confirmLabel="Escalate step" busy={busy} onCancel={() => setEscalating(false)} onConfirm={() => { setBusy(true); setError(""); void escalateApproval(settings, selected).then(() => { setEscalating(false); return reload(selected.id); }).catch((failure) => setError(text(failure, "The Approval step could not be escalated."))).finally(() => setBusy(false)); }} />}
|
|
|
|