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:
@@ -162,6 +162,8 @@ interface ProjectRow {
|
||||
totalPersonDays: number;
|
||||
utilizationPercent: number;
|
||||
dynamicFields?: Record<string, unknown> | null;
|
||||
coverImageUrl?: string | null;
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
// ─── Main component ───────────────────────────────────────────────────────────
|
||||
@@ -351,8 +353,21 @@ export function ProjectsClient() {
|
||||
case "name":
|
||||
return (
|
||||
<td key={col.key} className="max-w-xs truncate px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<Link href={`/projects/${project.id}`} className="transition hover:text-brand-600 hover:underline">
|
||||
{project.name}
|
||||
<Link href={`/projects/${project.id}`} className="inline-flex items-center gap-2 transition hover:text-brand-600 hover:underline">
|
||||
{project.coverImageUrl ? (
|
||||
<img src={project.coverImageUrl} alt="" className="h-6 w-6 flex-shrink-0 rounded object-cover" />
|
||||
) : (
|
||||
<span
|
||||
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded text-[9px] font-bold opacity-60"
|
||||
style={{
|
||||
backgroundColor: (project.color ?? "#6366f1") + "22",
|
||||
color: project.color ?? "#6366f1",
|
||||
}}
|
||||
>
|
||||
{project.name.split(/\s+/).map((w) => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate">{project.name}</span>
|
||||
</Link>
|
||||
</td>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,16 @@ import { notFound } from "next/navigation";
|
||||
import { formatDate } from "~/lib/format.js";
|
||||
import Link from "next/link";
|
||||
import { createCaller } from "~/server/trpc.js";
|
||||
import { auth } from "~/server/auth.js";
|
||||
import { BudgetStatusCard } from "~/components/projects/BudgetStatusCard.js";
|
||||
import { ProjectDetailActions } from "~/components/projects/ProjectDetailClient.js";
|
||||
import { ProjectDemandsTable } from "~/components/projects/ProjectDemandsTable.js";
|
||||
import { ProjectAssignmentsTable } from "~/components/projects/ProjectAssignmentsTable.js";
|
||||
import { PROJECT_STATUS_BADGE as STATUS_COLORS, ORDER_TYPE_BADGE as ORDER_TYPE_COLORS } from "~/lib/status-styles.js";
|
||||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||||
import { CoverArtSection } from "~/components/projects/CoverArtSection.js";
|
||||
|
||||
const EDIT_ROLES = new Set(["ADMIN", "MANAGER"]);
|
||||
|
||||
interface ProjectDetailPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -14,7 +19,9 @@ interface ProjectDetailPageProps {
|
||||
|
||||
export default async function ProjectDetailPage({ params }: ProjectDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const trpc = await createCaller();
|
||||
const [trpc, session] = await Promise.all([createCaller(), auth()]);
|
||||
const userRole = (session?.user as { role?: string } | undefined)?.role ?? "USER";
|
||||
const canEditProject = EDIT_ROLES.has(userRole);
|
||||
|
||||
let project: Awaited<ReturnType<typeof trpc.project.getById>>;
|
||||
try {
|
||||
@@ -41,8 +48,18 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro
|
||||
Back to Projects
|
||||
</Link>
|
||||
|
||||
{/* Cover Art */}
|
||||
<CoverArtSection
|
||||
projectId={project.id}
|
||||
coverImageUrl={project.coverImageUrl}
|
||||
coverFocusY={project.coverFocusY}
|
||||
projectColor={project.color}
|
||||
projectName={project.name}
|
||||
canEdit={canEditProject}
|
||||
/>
|
||||
|
||||
{/* Project header */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
@@ -50,9 +67,11 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro
|
||||
<span className={`inline-block px-2 py-0.5 text-xs rounded-full ${STATUS_COLORS[project.status] ?? ""}`}>
|
||||
{project.status}
|
||||
</span>
|
||||
<InfoTooltip content="Project lifecycle status: DRAFT = not yet visible, ACTIVE = in progress, ON_HOLD = paused, COMPLETED = finished, CANCELLED = abandoned." />
|
||||
<span className={`inline-block px-2 py-0.5 text-xs rounded-full ${ORDER_TYPE_COLORS[project.orderType] ?? ""}`}>
|
||||
{project.orderType}
|
||||
</span>
|
||||
<InfoTooltip content="BD = Business Development (pre-sales), CHARGEABLE = billable client work, INTERNAL = internal project, OVERHEAD = non-project overhead." />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">{project.name}</h1>
|
||||
</div>
|
||||
@@ -63,7 +82,7 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro
|
||||
{" — "}
|
||||
{formatDate(project.endDate)}
|
||||
</div>
|
||||
<div className="mt-0.5">Win probability: {project.winProbability}%</div>
|
||||
<div className="mt-0.5 flex items-center">Win probability: {project.winProbability}%<InfoTooltip content="Likelihood of winning this project (0-100%). Used to calculate weighted pipeline value (budget x probability)." /></div>
|
||||
</div>
|
||||
<ProjectDetailActions project={project as never} />
|
||||
</div>
|
||||
@@ -71,30 +90,30 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro
|
||||
|
||||
<dl className="grid grid-cols-2 gap-x-8 gap-y-3 sm:grid-cols-4 mt-4 pt-4 border-t border-gray-100">
|
||||
<div>
|
||||
<dt className="text-xs text-gray-500">Chargecode</dt>
|
||||
<dt className="text-xs text-gray-500 flex items-center">Chargecode<InfoTooltip content="Unique project identifier used for time tracking and cost attribution." /></dt>
|
||||
<dd className="mt-0.5 text-sm font-mono font-medium text-gray-900">{project.shortCode}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-gray-500">Order Type</dt>
|
||||
<dt className="text-xs text-gray-500 flex items-center">Order Type<InfoTooltip content="BD = Business Development, CHARGEABLE = billable client work, INTERNAL = internal project, OVERHEAD = overhead costs." /></dt>
|
||||
<dd className="mt-0.5 text-sm text-gray-900">{project.orderType}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-gray-500">Allocation Type</dt>
|
||||
<dt className="text-xs text-gray-500 flex items-center">Allocation Type<InfoTooltip content="INT = staffed with internal resources, EXT = staffed with external contractors." /></dt>
|
||||
<dd className="mt-0.5 text-sm text-gray-900">{project.allocationType}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-gray-500">Assignments</dt>
|
||||
<dt className="text-xs text-gray-500 flex items-center">Assignments<InfoTooltip content="Number of active resource assignments (confirmed or in-progress allocations) on this project." /></dt>
|
||||
<dd className="mt-0.5 text-sm text-gray-900">{activeAssignments.length} active</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-gray-500">Open Demands</dt>
|
||||
<dt className="text-xs text-gray-500 flex items-center">Open Demands<InfoTooltip content="Staffing requirements that still need resources. Unfilled seats are demand positions not yet assigned to a person." /></dt>
|
||||
<dd className="mt-0.5 text-sm text-gray-900">
|
||||
{activeDemands.length} items · {unfilledSeats}/{requestedSeats} seats unfilled
|
||||
</dd>
|
||||
</div>
|
||||
{project.responsiblePerson && (
|
||||
<div className="sm:col-span-2">
|
||||
<dt className="text-xs text-gray-500">Responsible Person</dt>
|
||||
<dt className="text-xs text-gray-500 flex items-center">Responsible Person<InfoTooltip content="The project lead or account manager responsible for this project." /></dt>
|
||||
<dd className="mt-0.5 text-sm font-medium text-gray-900">{project.responsiblePerson}</dd>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user