import { useEffect, useMemo, useRef, useState } from "react"; import { Check, RefreshCw } from "lucide-react"; import type { ApiSettings } from "@govoplan/core-webui"; import { AdminPageLayout, Button, Card, StatusBadge, adminErrorMessage, formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks } from "@govoplan/core-webui"; import { approveConfigurationChangeRequest, fetchConfigurationChangesDelta, type ConfigurationChangeRecord, type ConfigurationChangeRequest } from "../../api/admin"; const DELTA_KEY = "admin:configuration-changes"; export default function ConfigurationChangesPanel({ settings, canApprove }: {settings: ApiSettings;canApprove: boolean;}) { const [requests, setRequests] = useState([]); const [history, setHistory] = useState([]); const requestsRef = useRef([]); const historyRef = useRef([]); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const [loading, setLoading] = useState(true); const [busyId, setBusyId] = useState(""); const [error, setError] = useState(""); const [message, setMessage] = useState(""); async function load() { setLoading(true); setError(""); try { let nextWatermark = getDeltaWatermark(DELTA_KEY); let nextRequests = requestsRef.current; let nextHistory = historyRef.current; let hasMore = false; do { const payload = await fetchConfigurationChangesDelta(settings, { since: nextWatermark }); nextRequests = payload.full ? payload.requests : mergeDeltaRows(nextRequests, payload.requests, payload.deleted, (request) => request.id, { sort: sortConfigurationRequests }); nextHistory = payload.full ? payload.history : mergeDeltaRows(nextHistory, payload.history, payload.deleted, (record) => record.id, { sort: sortConfigurationHistory }); nextWatermark = payload.watermark ?? null; hasMore = payload.has_more; } while (hasMore); requestsRef.current = nextRequests; historyRef.current = nextHistory; setRequests(nextRequests); setHistory(nextHistory); setDeltaWatermark(DELTA_KEY, nextWatermark); } catch (err) { setError(adminErrorMessage(err)); } finally { setLoading(false); } } useEffect(() => { requestsRef.current = []; historyRef.current = []; resetDeltaWatermark(DELTA_KEY); void load(); }, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]); async function approve(request: ConfigurationChangeRequest) { setBusyId(request.id); setError(""); setMessage(""); try { await approveConfigurationChangeRequest(settings, request.id); setMessage(i18nMessage("i18n:govoplan-admin.approved_value.52da808b", { value0: request.key })); await load(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusyId(""); } } const pending = useMemo(() => requests.filter((item) => item.status !== "applied" && item.status !== "rejected"), [requests]); return ( void load()} disabled={loading}> i18n:govoplan-admin.reload.cce71553}>
{pending.map((request) => )} {!pending.length && }
i18n:govoplan-admin.setting.fb449f71 i18n:govoplan-admin.status.bae7d5be i18n:govoplan-admin.requested.c26bf60f i18n:govoplan-admin.approvals.deb9d03c i18n:govoplan-admin.target.61ad50a9 i18n:govoplan-admin.action.97c89a4d
{request.label || request.key}{request.key} {formatDateTime(request.requested_at)} {request.approvals.length} {targetLabel(request.target)} {canApprove && request.status === "pending_approval" && }
i18n:govoplan-admin.no_open_requests.580c95f9
{history.map((record) => )} {!history.length && }
i18n:govoplan-admin.version.2da600bf i18n:govoplan-admin.setting.fb449f71 i18n:govoplan-admin.status.bae7d5be i18n:govoplan-admin.applied.a3e4a569 i18n:govoplan-admin.approvers.0e2de1fb i18n:govoplan-admin.target.61ad50a9
#{record.version} {record.key}{record.id} {formatDateTime(record.created_at)} {record.approval_user_ids.length || "-"} {targetLabel(record.target)}
i18n:govoplan-admin.no_applied_configuration_changes.6a3ae4a7
); } function statusTone(status: string): string { if (status === "approved" || status === "applied") return "success"; if (status === "pending_approval") return "warning"; if (status === "rejected") return "error"; return "inactive"; } function targetLabel(target?: Record): string { if (!target || !Object.keys(target).length) return "-"; return Object.entries(target).map(([key, value]) => `${key}: ${String(value)}`).join(", "); } function sortConfigurationRequests(left: ConfigurationChangeRequest, right: ConfigurationChangeRequest): number { return new Date(right.updated_at || right.requested_at).getTime() - new Date(left.updated_at || left.requested_at).getTime(); } function sortConfigurationHistory(left: ConfigurationChangeRecord, right: ConfigurationChangeRecord): number { return right.version - left.version; }