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
@@ -9,6 +9,7 @@ import { uuid } from "~/lib/uuid.js";
import { DateInput } from "~/components/ui/DateInput.js";
import { SkillTagInput } from "~/components/ui/SkillTagInput.js";
import { usePermissions } from "~/hooks/usePermissions.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
// ─── Constants ────────────────────────────────────────────────────────────────
@@ -186,7 +187,7 @@ function Step1({ state, onChange }: Step1Props) {
<div className="space-y-5">
{/* Blueprint picker */}
<div>
<label className={LABEL_CLS}>Project Blueprint (optional)</label>
<label className={LABEL_CLS}>Project Blueprint (optional)<InfoTooltip content="Blueprints are templates that pre-fill role presets and default settings. Selecting one loads staffing requirements into Step 3." /></label>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 mb-2">
<button
type="button"
@@ -238,7 +239,7 @@ function Step1({ state, onChange }: Step1Props) {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Short code */}
<div>
<label className={LABEL_CLS}>Short Code *</label>
<label className={LABEL_CLS}>Short Code *<InfoTooltip content="Unique chargecode for this project, used for time tracking and cost attribution. Must be uppercase alphanumeric." /></label>
<input
type="text"
value={state.shortCode}
@@ -251,7 +252,7 @@ function Step1({ state, onChange }: Step1Props) {
{/* Name */}
<div>
<label className={LABEL_CLS}>Project Name *</label>
<label className={LABEL_CLS}>Project Name *<InfoTooltip content="Display name shown on the timeline and in reports." /></label>
<input
type="text"
value={state.name}
@@ -263,7 +264,7 @@ function Step1({ state, onChange }: Step1Props) {
{/* Order type */}
<div>
<label className={LABEL_CLS}>Order Type *</label>
<label className={LABEL_CLS}>Order Type *<InfoTooltip content="BD = Business Development (pre-sales), CHARGEABLE = billable client work, INTERNAL = internal project, OVERHEAD = non-project overhead." /></label>
<select
value={state.orderType}
onChange={(e) => onChange({ orderType: e.target.value })}
@@ -279,7 +280,7 @@ function Step1({ state, onChange }: Step1Props) {
{/* Allocation type */}
<div>
<label className={LABEL_CLS}>Allocation Type *</label>
<label className={LABEL_CLS}>Allocation Type *<InfoTooltip content="INT = staffed with internal resources, EXT = staffed with external contractors or freelancers." /></label>
<select
value={state.allocationType}
onChange={(e) => onChange({ allocationType: e.target.value })}
@@ -392,14 +393,14 @@ function Step2({ state, onChange }: Step2Props) {
<div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={LABEL_CLS}>Start Date *</label>
<label className={LABEL_CLS}>Start Date *<InfoTooltip content="First day of the project period. Assignments and budget tracking begin from this date." /></label>
<DateInput
value={state.startDate}
onChange={(v) => onChange({ startDate: v })}
/>
</div>
<div>
<label className={LABEL_CLS}>End Date *</label>
<label className={LABEL_CLS}>End Date *<InfoTooltip content="Last day of the project period. Defines the timeline boundary for all assignments." /></label>
<DateInput
value={state.endDate}
onChange={(v) => onChange({ endDate: v })}
@@ -409,7 +410,7 @@ function Step2({ state, onChange }: Step2Props) {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className={LABEL_CLS}>Budget (EUR) *</label>
<label className={LABEL_CLS}>Budget (EUR) *<InfoTooltip content="Total project budget in EUR. Stored internally as cents. Used to track spending against assignments." /></label>
<input
type="number"
min={0}
@@ -421,7 +422,7 @@ function Step2({ state, onChange }: Step2Props) {
/>
</div>
<div>
<label className={LABEL_CLS}>Responsible Person</label>
<label className={LABEL_CLS}>Responsible Person<InfoTooltip content="Project lead or account manager. Search by name or employee ID." /></label>
<ResourcePersonPicker
value={state.responsiblePerson}
onChange={(v) => onChange({ responsiblePerson: v })}
@@ -432,6 +433,7 @@ function Step2({ state, onChange }: Step2Props) {
<div>
<label className={LABEL_CLS}>
Win Probability: <strong>{state.winProbability}%</strong>
<InfoTooltip content="Likelihood of winning this project (0-100%). Affects the weighted pipeline value: budget x probability." />
</label>
<input
type="range"
@@ -522,7 +524,7 @@ function Step3({ state, onChange }: Step3Props) {
<div key={req.id} className="border border-gray-200 rounded-lg p-3 bg-white">
<div className="flex flex-wrap items-start gap-2">
<div className="flex-1 min-w-32">
<label className="text-xs text-gray-400">Role *</label>
<label className="text-xs text-gray-400">Role *<InfoTooltip content="Select a predefined role or enter a custom role name. Defines the skill profile for this staffing demand." /></label>
{roles.length > 0 ? (
<select
value={req.roleId ?? ""}
@@ -557,7 +559,7 @@ function Step3({ state, onChange }: Step3Props) {
)}
</div>
<div className="w-20">
<label className="text-xs text-gray-400">h/day</label>
<label className="text-xs text-gray-400">h/day<InfoTooltip content="Planned working hours per day for this role." /></label>
<input
type="number"
value={req.hoursPerDay}
@@ -569,7 +571,7 @@ function Step3({ state, onChange }: Step3Props) {
/>
</div>
<div className="w-16">
<label className="text-xs text-gray-400">Count</label>
<label className="text-xs text-gray-400">Count<InfoTooltip content="Number of people needed for this role. Unfilled seats become placeholder demands until assigned." /></label>
<input
type="number"
value={req.headcount}
@@ -580,7 +582,7 @@ function Step3({ state, onChange }: Step3Props) {
/>
</div>
<div className="w-28">
<label className="text-xs text-gray-400">Budget (EUR)</label>
<label className="text-xs text-gray-400">Budget (EUR)<InfoTooltip content="Optional budget cap for this role. Tracked against actual assignment costs." /></label>
<input
type="number"
value={req.budgetCents ? req.budgetCents / 100 : ""}
@@ -606,7 +608,7 @@ function Step3({ state, onChange }: Step3Props) {
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2">
<div>
<label className="text-xs text-gray-400">Required skills</label>
<label className="text-xs text-gray-400">Required skills<InfoTooltip content="Skills a resource must have to be suggested for this role." /></label>
<SkillTagInput
value={req.requiredSkills}
onChange={(skills) => updateReq(idx, { requiredSkills: skills })}
@@ -614,7 +616,7 @@ function Step3({ state, onChange }: Step3Props) {
/>
</div>
<div>
<label className="text-xs text-gray-400">Preferred skills (optional)</label>
<label className="text-xs text-gray-400">Preferred skills (optional)<InfoTooltip content="Nice-to-have skills that boost a resource's match score but are not mandatory." /></label>
<SkillTagInput
value={req.preferredSkills ?? []}
onChange={(skills) => updateReq(idx, { preferredSkills: skills })}
@@ -622,7 +624,7 @@ function Step3({ state, onChange }: Step3Props) {
/>
</div>
<div>
<label className="text-xs text-gray-400">Chapter filter (optional)</label>
<label className="text-xs text-gray-400">Chapter filter (optional)<InfoTooltip content="Restrict suggestions to resources from a specific chapter/department." /></label>
<input
type="text"
value={req.chapter ?? ""}
@@ -838,6 +840,10 @@ function Step4({ state, onChange }: Step4Props) {
return (
<div className="space-y-5 max-h-[55vh] overflow-y-auto pr-1">
<p className="text-xs text-gray-500 flex items-center gap-1">
AI-powered resource suggestions based on skills, availability, and utilization.
<InfoTooltip content="Resources are ranked by skill match score, current utilization, and availability in the project period. Assign resources here or leave unfilled to create placeholder demands." />
</p>
{state.staffingReqs.map((req) => (
<div key={req.id} className="border border-gray-200 rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
@@ -876,6 +882,10 @@ function Step5({ state, onChange, onSubmit, isSubmitting, submitError }: Step5Pr
return (
<div className="space-y-4">
{/* Project summary */}
<div className="flex items-center gap-1 mb-1">
<p className="text-xs font-medium text-gray-600 uppercase tracking-wider">Project Summary</p>
<InfoTooltip content="Review all project details before creation. The project, staffing demands, and any pre-assigned resources will be created together." />
</div>
<div className="bg-gray-50 rounded-lg p-4 text-sm space-y-2">
<div className="grid grid-cols-2 gap-x-6 gap-y-1">
<div>
@@ -941,8 +951,9 @@ function Step5({ state, onChange, onSubmit, isSubmitting, submitError }: Step5Pr
{/* Draft toggle */}
<div className="border border-gray-200 rounded-lg p-4">
<p className="text-xs font-medium text-gray-600 uppercase tracking-wider mb-3">
<p className="text-xs font-medium text-gray-600 uppercase tracking-wider mb-3 flex items-center gap-1">
Save as
<InfoTooltip content="Draft projects are hidden from the timeline until activated. Active projects appear on the timeline immediately." />
</p>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">