Add organization governance controls
This commit is contained in:
1 parent
1d7757c88b
commit
7116ae2a7b
15 files changed
+589
-5
No files matched your search
@@ -79,6 +79,10 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
const employeeIdentities = useMemo(() => snapshot.employeeIdentities.filter((item) => item.orgId === orgId), [snapshot.employeeIdentities, orgId]);
|
||||
const serviceIdentities = useMemo(() => snapshot.serviceIdentities.filter((item) => item.orgId === orgId), [snapshot.serviceIdentities, orgId]);
|
||||
const roleGrants = useMemo(() => snapshot.roleGrants.filter((item) => item.orgId === orgId), [snapshot.roleGrants, orgId]);
|
||||
const orgDepartments = useMemo(() => snapshot.orgDepartments.filter((item) => item.orgId === orgId), [snapshot.orgDepartments, orgId]);
|
||||
const hiringPolicies = useMemo(() => snapshot.hiringPolicies.filter((item) => item.orgId === orgId), [snapshot.hiringPolicies, orgId]);
|
||||
const approvalRoutingRules = useMemo(() => snapshot.approvalRoutingRules.filter((item) => item.orgId === orgId), [snapshot.approvalRoutingRules, orgId]);
|
||||
const approvalAssignments = useMemo(() => snapshot.approvalAssignments.filter((item) => item.orgId === orgId), [snapshot.approvalAssignments, orgId]);
|
||||
|
||||
return (
|
||||
<div className="split-main" style={{ height: "calc(100vh - 44px)", margin: "-28px -32px -40px" }}>
|
||||
@@ -124,7 +128,17 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
{tab === "policies" && <PoliciesPanel policies={policies} />}
|
||||
{tab === "policies" && (
|
||||
<PoliciesPanel
|
||||
policies={policies}
|
||||
agents={agents}
|
||||
employees={employees}
|
||||
orgDepartments={orgDepartments}
|
||||
hiringPolicies={hiringPolicies}
|
||||
approvalRoutingRules={approvalRoutingRules}
|
||||
approvalAssignments={approvalAssignments}
|
||||
/>
|
||||
)}
|
||||
{tab === "compliance" && (
|
||||
<CompliancePanel
|
||||
orgId={orgId}
|
||||
|
||||
@@ -1,11 +1,37 @@
|
||||
import { Pill } from "../../shared/ui";
|
||||
import type { PolicyRuleRecord } from "../../../../shared/domain";
|
||||
import type {
|
||||
AgentRecord,
|
||||
ApprovalAssignmentRecord,
|
||||
ApprovalRoutingRuleRecord,
|
||||
EmployeeRecord,
|
||||
HiringPolicyRecord,
|
||||
OrgDepartmentRecord,
|
||||
PolicyRuleRecord,
|
||||
} from "../../../../shared/domain";
|
||||
import { money } from "../types";
|
||||
|
||||
export function PoliciesPanel({ policies }: { policies: PolicyRuleRecord[] }) {
|
||||
export function PoliciesPanel({
|
||||
policies,
|
||||
agents,
|
||||
employees,
|
||||
orgDepartments,
|
||||
hiringPolicies,
|
||||
approvalRoutingRules,
|
||||
approvalAssignments,
|
||||
}: {
|
||||
policies: PolicyRuleRecord[];
|
||||
agents: AgentRecord[];
|
||||
employees: EmployeeRecord[];
|
||||
orgDepartments: OrgDepartmentRecord[];
|
||||
hiringPolicies: HiringPolicyRecord[];
|
||||
approvalRoutingRules: ApprovalRoutingRuleRecord[];
|
||||
approvalAssignments: ApprovalAssignmentRecord[];
|
||||
}) {
|
||||
return (
|
||||
<div className="page">
|
||||
<h1 className="page-h1">Policies</h1>
|
||||
<p className="page-sub">Policy rules evaluated for actions across the workspace.</p>
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Rules</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div
|
||||
className="table-row head"
|
||||
@@ -30,6 +56,118 @@ export function PoliciesPanel({ policies }: { policies: PolicyRuleRecord[] }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="section-label" style={{ margin: "24px 0 10px" }}>Hiring policies</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div
|
||||
className="table-row head"
|
||||
style={{ gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1.4fr) minmax(0, 1fr)" }}
|
||||
>
|
||||
<div>Department</div><div>Headcount</div><div>Budget cap</div><div>Review</div>
|
||||
</div>
|
||||
{hiringPolicies.map((policy, index) => (
|
||||
<HiringPolicyRow
|
||||
key={policy.id}
|
||||
policy={policy}
|
||||
department={orgDepartments.find((item) => item.name === policy.departmentName) ?? null}
|
||||
agents={agents}
|
||||
last={index === hiringPolicies.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="section-label" style={{ margin: "24px 0 10px" }}>Review routing</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div
|
||||
className="table-row head"
|
||||
style={{ gridTemplateColumns: "minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1.4fr) minmax(0, 1fr)" }}
|
||||
>
|
||||
<div>Kind</div><div>Min risk</div><div>Route</div><div>Assigned</div>
|
||||
</div>
|
||||
{approvalRoutingRules.map((rule, index) => (
|
||||
<RoutingRuleRow
|
||||
key={rule.id}
|
||||
rule={rule}
|
||||
employees={employees}
|
||||
agents={agents}
|
||||
assignmentCount={approvalAssignments.filter((assignment) => assignment.routingRuleId === rule.id).length}
|
||||
last={index === approvalRoutingRules.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HiringPolicyRow({
|
||||
policy,
|
||||
department,
|
||||
agents,
|
||||
last,
|
||||
}: {
|
||||
policy: HiringPolicyRecord;
|
||||
department: OrgDepartmentRecord | null;
|
||||
agents: AgentRecord[];
|
||||
last: boolean;
|
||||
}) {
|
||||
if (!department) throw new Error(`Department governance is missing for ${policy.departmentName}.`);
|
||||
const roster = agents.filter((agent) => agent.department === policy.departmentName && agent.lifecycle === "active");
|
||||
const budgetCents = roster.reduce((sum, agent) => sum + agent.budgetCents, 0);
|
||||
return (
|
||||
<div
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1.4fr) minmax(0, 1fr)",
|
||||
borderBottom: last ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 540 }}>{policy.departmentName}</div>
|
||||
<div className="num mono">{roster.length} / {department.headcountLimit}</div>
|
||||
<div className="num mono">{money(budgetCents)} / {money(policy.monthlyBudgetLimitCents)}</div>
|
||||
<div><Pill>{money(policy.requireApprovalAboveCents)}</Pill></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RoutingRuleRow({
|
||||
rule,
|
||||
employees,
|
||||
agents,
|
||||
assignmentCount,
|
||||
last,
|
||||
}: {
|
||||
rule: ApprovalRoutingRuleRecord;
|
||||
employees: EmployeeRecord[];
|
||||
agents: AgentRecord[];
|
||||
assignmentCount: number;
|
||||
last: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1.4fr) minmax(0, 1fr)",
|
||||
borderBottom: last ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 540 }}>{rule.kind}</div>
|
||||
<div><Pill tone={rule.minimumRisk === "high" ? "red" : rule.minimumRisk === "medium" ? "amber" : undefined}>{rule.minimumRisk}</Pill></div>
|
||||
<div style={{ color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{routeLabel(rule, employees, agents)}
|
||||
</div>
|
||||
<div className="num mono">{assignmentCount}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function routeLabel(rule: ApprovalRoutingRuleRecord, employees: EmployeeRecord[], agents: AgentRecord[]): string {
|
||||
if (rule.routeToType === "board") return "Board";
|
||||
if (rule.routeToType === "employee") {
|
||||
const employee = employees.find((item) => item.id === rule.routeToId);
|
||||
if (!employee) throw new Error("Approval route employee is missing.");
|
||||
return employee.displayName;
|
||||
}
|
||||
const agent = agents.find((item) => item.id === rule.routeToId);
|
||||
if (!agent) throw new Error("Approval route agent is missing.");
|
||||
return agent.name;
|
||||
}
|
||||
Reference in new issue
Block a user