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:
@@ -237,6 +237,7 @@ export function ChargeabilityWidget({ config: _config }: WidgetProps) {
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
Include proposed
|
||||
<InfoTooltip content="When enabled, PROPOSED bookings and imported TBD planning rows are also counted toward chargeability." />
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -248,6 +249,7 @@ export function ChargeabilityWidget({ config: _config }: WidgetProps) {
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
Show departed
|
||||
<InfoTooltip content="When enabled, resources who have left the company are included in the lists." />
|
||||
</label>
|
||||
<FilterDropdown label={selectedCountryLabel}>
|
||||
<div className="space-y-2">
|
||||
@@ -295,8 +297,9 @@ export function ChargeabilityWidget({ config: _config }: WidgetProps) {
|
||||
handleSectionScroll(event, topVisibleCount, top.length, setTopVisibleCount)
|
||||
}
|
||||
>
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider px-1 mb-1 sticky top-0 bg-white">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider px-1 mb-1 sticky top-0 bg-white flex items-center">
|
||||
Top Chargeability
|
||||
<InfoTooltip content="Resources ranked by highest actual chargeability this month. Chargeability = chargeable booked hours / total available hours." />
|
||||
<span className="ml-1 font-normal normal-case text-gray-400">
|
||||
{visibleTop.length}/{top.length}
|
||||
</span>
|
||||
@@ -380,8 +383,9 @@ export function ChargeabilityWidget({ config: _config }: WidgetProps) {
|
||||
handleSectionScroll(event, watchVisibleCount, watchlist.length, setWatchVisibleCount)
|
||||
}
|
||||
>
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider px-1 mb-1 sticky top-0 bg-white">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider px-1 mb-1 sticky top-0 bg-white flex items-center">
|
||||
Watchlist <span className="font-normal text-gray-400">(below target)</span>
|
||||
<InfoTooltip content="Resources whose actual chargeability is more than 15 percentage points below their individual target. These may need more project assignments." />
|
||||
<span className="ml-1 font-normal normal-case text-gray-400">
|
||||
{visibleWatchlist.length}/{watchlist.length}
|
||||
</span>
|
||||
|
||||
@@ -92,10 +92,15 @@ export function DemandWidget({ config, onConfigChange }: WidgetProps) {
|
||||
<thead className="bg-gray-50 sticky top-0">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<button type="button" onClick={() => toggleSort("name")} className="inline-flex items-center hover:text-gray-700 cursor-pointer">
|
||||
{groupBy === "project" ? "Project" : groupBy === "person" ? "Person" : "Chapter"}
|
||||
<Ind k="name" />
|
||||
</button>
|
||||
<span className="inline-flex items-center">
|
||||
<button type="button" onClick={() => toggleSort("name")} className="inline-flex items-center hover:text-gray-700 cursor-pointer">
|
||||
{groupBy === "project" ? "Project" : groupBy === "person" ? "Person" : "Chapter"}
|
||||
<Ind k="name" />
|
||||
</button>
|
||||
{groupBy === "project" && <InfoTooltip content="Active projects with allocations in the current quarter. Short code shown before the name." />}
|
||||
{groupBy === "person" && <InfoTooltip content="Resources with active allocations in the current quarter." />}
|
||||
{groupBy === "chapter" && <InfoTooltip content="Organizational chapters (teams/departments) with active allocations in the current quarter." />}
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-gray-500">
|
||||
<span className="inline-flex items-center justify-end">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import type { WidgetProps } from "../widget-registry.js";
|
||||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||||
|
||||
const STATUS_DOT: Record<string, string> = {
|
||||
ACTIVE: "bg-green-500",
|
||||
@@ -78,8 +79,9 @@ export function MyProjectsWidget({ config }: WidgetProps) {
|
||||
<div className="flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{favoriteProjects.length > 0 && (
|
||||
<div>
|
||||
<div className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400 bg-amber-50/50 dark:bg-amber-900/10">
|
||||
<div className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400 bg-amber-50/50 dark:bg-amber-900/10 flex items-center">
|
||||
Favorites
|
||||
<InfoTooltip content="Projects you have starred. Click the star icon on any project to add or remove it from your favorites." />
|
||||
</div>
|
||||
{favoriteProjects.map((p) => (
|
||||
<ProjectRow key={p.id} project={p} isFavorite onToggleFavorite={() => toggleMutation.mutate({ projectId: p.id })} />
|
||||
@@ -89,8 +91,9 @@ export function MyProjectsWidget({ config }: WidgetProps) {
|
||||
|
||||
{responsibleProjects.length > 0 && (
|
||||
<div>
|
||||
<div className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-brand-600 dark:text-brand-400 bg-brand-50/50 dark:bg-brand-900/10">
|
||||
<div className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-brand-600 dark:text-brand-400 bg-brand-50/50 dark:bg-brand-900/10 flex items-center">
|
||||
Responsible
|
||||
<InfoTooltip content="Projects where you are listed as the responsible person. These are automatically shown based on your user name." />
|
||||
</div>
|
||||
{responsibleProjects.map((p) => (
|
||||
<ProjectRow key={p.id} project={p} isFavorite={false} onToggleFavorite={() => toggleMutation.mutate({ projectId: p.id })} />
|
||||
@@ -130,7 +133,7 @@ function ProjectRow({
|
||||
<span className="font-mono text-xs text-gray-400 dark:text-gray-500 mr-1.5">{project.shortCode}</span>
|
||||
{project.name}
|
||||
</Link>
|
||||
<span className="flex-shrink-0 text-[10px] text-gray-400 dark:text-gray-500 uppercase">{project.status}</span>
|
||||
<span className="flex-shrink-0 text-[10px] text-gray-400 dark:text-gray-500 uppercase" title={`Project status: ${project.status}`}>{project.status}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,64 +121,73 @@ export function ProjectTableWidget({ config, onConfigChange }: WidgetProps) {
|
||||
<thead className="sticky top-0">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("code")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Code
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "code" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
<span className="inline-flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("code")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Code
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "code" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<InfoTooltip content="Unique short code identifying the project (e.g. PRJ-001)." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("name")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Name
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "name" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
<span className="inline-flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("name")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Name
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "name" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<InfoTooltip content="Project name. Click to open the project detail page." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("status")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Status
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "status" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
<span className="inline-flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("status")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Status
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "status" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<InfoTooltip content="Current project lifecycle status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, or CANCELLED." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-gray-500">
|
||||
<span className="inline-flex items-center justify-end">
|
||||
@@ -230,7 +239,10 @@ export function ProjectTableWidget({ config, onConfigChange }: WidgetProps) {
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-gray-500">
|
||||
Budget
|
||||
<span className="inline-flex items-center justify-end">
|
||||
Budget
|
||||
<InfoTooltip content="Project budget in EUR. The colored dot indicates utilization: green = healthy, amber = above 80%, red = over budget." />
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -122,6 +122,7 @@ export function ResourceTableWidget({ config, onConfigChange }: WidgetProps) {
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
Include proposed
|
||||
<InfoTooltip content="When enabled, PROPOSED bookings are counted toward booking count and utilization." />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -154,44 +155,50 @@ export function ResourceTableWidget({ config, onConfigChange }: WidgetProps) {
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("name")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Name
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "name" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
<span className="inline-flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("name")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Name
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "name" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<InfoTooltip content="Display name of the resource. Rows highlighted in amber indicate overbooking." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("chapter")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Chapter
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "chapter" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
<span className="inline-flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSort("chapter")}
|
||||
className="inline-flex items-center gap-0.5 hover:text-gray-700 cursor-pointer"
|
||||
>
|
||||
Chapter
|
||||
<span className="text-[10px] ml-0.5">
|
||||
{sortKey === "chapter" ? (
|
||||
sortDir === "asc" ? (
|
||||
"▲"
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
"▼"
|
||||
)
|
||||
) : (
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<span className="text-gray-300">⇅</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<InfoTooltip content="Organizational chapter (team/department) the resource belongs to." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-gray-500">
|
||||
<span className="inline-flex items-center justify-end">
|
||||
|
||||
@@ -74,21 +74,35 @@ export function TopValueWidget({ config }: WidgetProps) {
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-50 sticky top-0">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500 w-8">#</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<button type="button" onClick={() => toggleSort("eid")} className="inline-flex items-center hover:text-gray-700 cursor-pointer">
|
||||
EID<Ind k="eid" />
|
||||
</button>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500 w-8">
|
||||
<span className="inline-flex items-center">
|
||||
#
|
||||
<InfoTooltip content="Rank position based on the current sort order." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<button type="button" onClick={() => toggleSort("name")} className="inline-flex items-center hover:text-gray-700 cursor-pointer">
|
||||
Name<Ind k="name" />
|
||||
</button>
|
||||
<span className="inline-flex items-center">
|
||||
<button type="button" onClick={() => toggleSort("eid")} className="inline-flex items-center hover:text-gray-700 cursor-pointer">
|
||||
EID<Ind k="eid" />
|
||||
</button>
|
||||
<InfoTooltip content="Employee ID — unique identifier for each resource." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<button type="button" onClick={() => toggleSort("chapter")} className="inline-flex items-center hover:text-gray-700 cursor-pointer">
|
||||
Chapter<Ind k="chapter" />
|
||||
</button>
|
||||
<span className="inline-flex items-center">
|
||||
<button type="button" onClick={() => toggleSort("name")} className="inline-flex items-center hover:text-gray-700 cursor-pointer">
|
||||
Name<Ind k="name" />
|
||||
</button>
|
||||
<InfoTooltip content="Display name of the resource." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">
|
||||
<span className="inline-flex items-center">
|
||||
<button type="button" onClick={() => toggleSort("chapter")} className="inline-flex items-center hover:text-gray-700 cursor-pointer">
|
||||
Chapter<Ind k="chapter" />
|
||||
</button>
|
||||
<InfoTooltip content="Organizational chapter (team/department) the resource belongs to." />
|
||||
</span>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-gray-500">
|
||||
<span className="inline-flex items-center justify-end">
|
||||
|
||||
Reference in New Issue
Block a user