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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user