diff --git a/src/renderer/components/execution/ApprovalInspector.tsx b/src/renderer/components/execution/ApprovalInspector.tsx index 1b0df493..fe568251 100644 --- a/src/renderer/components/execution/ApprovalInspector.tsx +++ b/src/renderer/components/execution/ApprovalInspector.tsx @@ -1,11 +1,20 @@ import { useState } from "react" -import { Briefcase, CheckCircle, ClockCounterClockwise, Robot, Trash, XCircle } from "@phosphor-icons/react" +import { Briefcase, CheckCircle, Clock, ClockCounterClockwise, Robot, Trash, XCircle } from "@phosphor-icons/react" import type { AgentRecord, ApprovalRecord, TaskRecord } from "@shared/types" import { useT } from "../../i18n" import { formatMoney, formatTime } from "../../lib/formatters" import { unwrap } from "../../lib/desktop" import { ActionButton, ConfirmDialog, InlineNotice, StatusPill, TextArea } from "../ui" import { describeActionError, parseApprovalPayloadJson } from "./execution-utils" +import { SocialDraftPreview } from "./SocialDraftPreview" + +function getTypeBadgeClass(type: string): string { + if (type === "hire_agent") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" + if (type === "approve_ceo_strategy") return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]" + if (type === "secret_access") return "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" + if (type === "social_post") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" + return "bg-[color:var(--panel-soft)] text-[color:var(--muted)]" +} export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide, onDeleted }: { approval: ApprovalRecord; agents: AgentRecord[]; tasks: TaskRecord[]; companyId: string @@ -14,12 +23,61 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide const t = useT() const [decisionNote, setDecisionNote] = useState(""); const [confirmDelete, setConfirmDelete] = useState(false) const [actionError, setActionError] = useState(null); const [acting, setActing] = useState<"approved"|"rejected"|"revision_requested"|"delete"|null>(null) + const [scheduleMode, setScheduleMode] = useState<"immediate" | "scheduled">("immediate") + const [scheduledAt, setScheduledAt] = useState("") + const [scheduleError, setScheduleError] = useState(null) const requestingAgent = agents.find((a) => a.id === approval.requestedByAgentId) ?? null const relatedTask = tasks.find((t) => t.id === approval.relatedTaskId) ?? null const isDecided = approval.state !== "pending" + const isSocialPost = approval.type === "social_post" async function handleDelete() { setActionError(null); setActing("delete"); try { unwrap(await window.agentCompany.deleteApproval({ id: approval.id, companyId })); setConfirmDelete(false); await onDeleted() } catch (e) { setActionError(describeActionError(e)) } finally { setActing(null) } } - async function handleDecision(state: "approved"|"rejected"|"revision_requested") { setActionError(null); setActing(state); try { await onDecide(state, decisionNote || undefined) } catch (e) { setActionError(describeActionError(e)) } finally { setActing(null) } } + + async function handleDecision(state: "approved"|"rejected"|"revision_requested") { + setActionError(null) + setScheduleError(null) + + // Validate schedule if social_post and user picked "scheduled" + if (isSocialPost && state === "approved" && scheduleMode === "scheduled") { + if (!scheduledAt) { + setScheduleError("Please select a date and time for scheduling.") + return + } + const chosenTime = new Date(scheduledAt).getTime() + if (chosenTime <= Date.now()) { + setScheduleError("Scheduled time must be in the future.") + return + } + } + + setActing(state) + try { + // For social_post approvals with a schedule, include the schedule info in the decision note + let noteForDecision = decisionNote || undefined + if (isSocialPost && state === "approved" && scheduleMode === "scheduled" && scheduledAt) { + const scheduleInfo = `[scheduled:${new Date(scheduledAt).toISOString()}]` + noteForDecision = noteForDecision ? `${noteForDecision} ${scheduleInfo}` : scheduleInfo + } + await onDecide(state, noteForDecision) + } catch (e) { + setActionError(describeActionError(e)) + } finally { + setActing(null) + } + } + + // Compute minimum datetime for schedule picker: now + 5 minutes + const minDatetime = (() => { + const d = new Date(Date.now() + 5 * 60 * 1000) + // Format as YYYY-MM-DDTHH:mm for datetime-local input + const pad = (n: number) => String(n).padStart(2, "0") + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` + })() + + const socialPlatform = (() => { + if (!isSocialPost || !approval.payloadJson) return "other" + try { return (JSON.parse(approval.payloadJson) as { platform?: string }).platform ?? "other" } catch { return "other" } + })() return (
@@ -27,7 +85,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide {actionError && !confirmDelete ? : null}
- {approval.type.replaceAll("_", " ")} + {approval.type.replaceAll("_", " ")}
{approval.payloadSummary}
@@ -49,10 +107,75 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide {mgr ?
Reports to: {mgr.name}
: null}
) })() : null} {approval.type === "approve_ceo_strategy" ?
Organization Strategy
{approval.payloadSummary}
: null} + {isSocialPost && approval.payloadJson ? ( + + ) : null} + + {/* Schedule picker for social_post approvals */} + {isSocialPost && !isDecided ? ( +
+
+ + +
+ {scheduleMode === "scheduled" ? ( +
+ + { setScheduledAt(e.target.value); setScheduleError(null) }} + className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[13px] text-[color:var(--text)] focus:border-[color:var(--accent)] focus:outline-none" + /> +
+ ) : null} + {scheduleError ? : null} +
+ ) : null} + {!isDecided ?