feat: project cover art with AI generation, branding rename, RBAC fix, computation graph

- Add DALL-E cover art generation for projects (Azure OpenAI + standard OpenAI)
- CoverArtSection component with generate/upload/remove/focus-point controls
- Client-side image compression (10MB input → WebP/JPEG, max 1920px)
- DALL-E settings in admin panel (deployment, endpoint, API key)
- MCP assistant tools for cover art (generate_project_cover, remove_project_cover)
- Rename "Planarchy" → "plANARCHY" across all UI-facing text (13 files)
- Fix hardcoded canEdit={true} on project detail page — now checks user role
- Computation graph visualization (2D/3D) for calculation rules
- OG image and OpenGraph metadata

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-18 11:31:56 +01:00
parent 21af720f90
commit 093e13b88f
86 changed files with 5623 additions and 744 deletions
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { trpc } from "~/lib/trpc/client.js";
const TRIGGER_TYPES = ["SICK", "VACATION", "PUBLIC_HOLIDAY", "CUSTOM"] as const;
@@ -164,13 +165,27 @@ export function CalculationRulesClient() {
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Name</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Trigger</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Cost Effect</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Chargeability</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Scope</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Priority</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Active</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
<span className="flex items-center">Name <InfoTooltip content="A descriptive label for this rule. Use clear names so admins can quickly identify what each rule does." /></span>
</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
<span className="flex items-center">Trigger <InfoTooltip content="The absence type that activates this rule: Sick Leave, Vacation, Public Holiday, or Custom. Determines when the cost/chargeability logic applies." /></span>
</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
<span className="flex items-center">Cost Effect <InfoTooltip content="How this absence affects project costs. 'Charge to Project' bills the project, 'No Project Cost' absorbs the cost centrally, 'Reduced Cost' applies a percentage discount." /></span>
</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
<span className="flex items-center">Chargeability <InfoTooltip content="Whether the person's time counts as chargeable during this absence. 'Person Chargeable' includes the time in chargeability metrics; 'Not Chargeable' excludes it." /></span>
</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
<span className="flex items-center">Scope <InfoTooltip content="Limits the rule to a specific project or order type (BD, Chargeable, Internal, Overhead). 'Global' means the rule applies to all projects." /></span>
</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
<span className="flex items-center">Priority <InfoTooltip content="When multiple rules match the same absence, the one with the highest priority number wins. Use this to create specific overrides for certain projects." /></span>
</th>
<th className="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">
<span className="flex items-center">Active <InfoTooltip content="Only active rules are evaluated. Deactivate a rule to temporarily disable it without deleting." /></span>
</th>
<th className="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Actions</th>
</tr>
</thead>
@@ -233,7 +248,7 @@ export function CalculationRulesClient() {
</h2>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
<label className="mb-1 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">Name <InfoTooltip content="A unique, descriptive name for this calculation rule." /></label>
<input
value={editing.name}
onChange={(e) => setEditing({ ...editing, name: e.target.value })}
@@ -241,7 +256,7 @@ export function CalculationRulesClient() {
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
<label className="mb-1 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">Description <InfoTooltip content="Optional notes explaining when and why this rule exists." /></label>
<textarea
value={editing.description}
onChange={(e) => setEditing({ ...editing, description: e.target.value })}
@@ -251,7 +266,7 @@ export function CalculationRulesClient() {
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Trigger Type</label>
<label className="mb-1 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">Trigger Type <InfoTooltip content="The type of absence event that activates this rule." /></label>
<select
value={editing.triggerType}
onChange={(e) => setEditing({ ...editing, triggerType: e.target.value })}
@@ -261,7 +276,7 @@ export function CalculationRulesClient() {
</select>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Order Type (optional)</label>
<label className="mb-1 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">Order Type (optional) <InfoTooltip content="Restricts this rule to a specific order type. Leave as 'All' to apply globally." /></label>
<select
value={editing.orderType}
onChange={(e) => setEditing({ ...editing, orderType: e.target.value })}
@@ -274,7 +289,7 @@ export function CalculationRulesClient() {
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Cost Effect</label>
<label className="mb-1 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">Cost Effect <InfoTooltip content="Determines how costs are attributed during this absence. 'Charge' bills the project, 'Zero' removes cost, 'Reduce' applies a percentage reduction." /></label>
<select
value={editing.costEffect}
onChange={(e) => setEditing({ ...editing, costEffect: e.target.value })}
@@ -284,7 +299,7 @@ export function CalculationRulesClient() {
</select>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Chargeability</label>
<label className="mb-1 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">Chargeability <InfoTooltip content="Controls whether this absence counts toward the person's chargeability KPI." /></label>
<select
value={editing.chargeabilityEffect}
onChange={(e) => setEditing({ ...editing, chargeabilityEffect: e.target.value })}
@@ -296,8 +311,8 @@ export function CalculationRulesClient() {
</div>
{editing.costEffect === "REDUCE" && (
<div>
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Reduction Percent (0-100)
<label className="mb-1 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">
Reduction Percent (0-100) <InfoTooltip content="The percentage by which the cost is reduced. E.g. 50 means the project is charged half the normal rate." />
</label>
<input
type="number"
@@ -311,7 +326,7 @@ export function CalculationRulesClient() {
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Priority</label>
<label className="mb-1 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300">Priority <InfoTooltip content="Higher numbers take precedence when multiple rules match. Use 0 for default rules and higher values for specific overrides." /></label>
<input
type="number"
min={0}
@@ -328,7 +343,7 @@ export function CalculationRulesClient() {
onChange={(e) => setEditing({ ...editing, isActive: e.target.checked })}
className="rounded border-gray-300"
/>
Active
Active <InfoTooltip content="Inactive rules are ignored during cost calculations." />
</label>
</div>
</div>