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:
@@ -10,6 +10,7 @@ import { SkillRadarChart } from "./SkillRadarChart.js";
|
||||
import { AiSummaryCard } from "./AiSummaryCard.js";
|
||||
import { SkillMatrixUpload } from "./SkillMatrixUpload.js";
|
||||
import { usePermissions } from "~/hooks/usePermissions.js";
|
||||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||||
|
||||
interface ResourceDetailProps {
|
||||
resourceId: string;
|
||||
@@ -46,10 +47,10 @@ const allocationStatusColor: Record<string, string> = {
|
||||
CANCELLED: "bg-red-100 text-red-500",
|
||||
};
|
||||
|
||||
function StatCard({ label, value, sub }: { label: string; value: string | number; sub?: string }) {
|
||||
function StatCard({ label, value, sub, tooltip }: { label: string; value: string | number; sub?: string; tooltip?: string }) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-4">
|
||||
<div className="text-xs text-gray-500 mb-1">{label}</div>
|
||||
<div className="text-xs text-gray-500 mb-1 flex items-center">{label}{tooltip && <InfoTooltip content={tooltip} />}</div>
|
||||
<div className="text-xl font-bold text-gray-900">{value}</div>
|
||||
{sub && <div className="text-xs text-gray-400 mt-0.5">{sub}</div>}
|
||||
</div>
|
||||
@@ -276,22 +277,26 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
|
||||
<StatCard
|
||||
label="LCR"
|
||||
value={`${(resource.lcrCents / 100).toFixed(0)} ${resource.currency}/h`}
|
||||
tooltip="Loaded Cost Rate: fully-loaded hourly cost including salary, benefits, and overhead. Used in budget calculations."
|
||||
/>
|
||||
)}
|
||||
{canViewCosts && (
|
||||
<StatCard
|
||||
label="UCR"
|
||||
value={`${(resource.ucrCents / 100).toFixed(0)} ${resource.currency}/h`}
|
||||
tooltip="Unit Cost Rate: the rate charged to the client or project for this resource's time."
|
||||
/>
|
||||
)}
|
||||
<StatCard
|
||||
label="Chargeability Target"
|
||||
value={`${resource.chargeabilityTarget}%`}
|
||||
tooltip="The percentage of working time this resource is expected to spend on chargeable/billable work."
|
||||
/>
|
||||
{canViewCosts && (
|
||||
<StatCard
|
||||
label="Actual (this month)"
|
||||
value={chargeStats != null ? `${chargeStats.actualChargeability}%` : "—"}
|
||||
tooltip="Actual chargeability = chargeable hours / total available hours x 100 for the current month."
|
||||
sub={
|
||||
includeProposedChargeability
|
||||
? "Incl. proposed + imported TBD planning"
|
||||
@@ -303,12 +308,14 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
|
||||
<StatCard
|
||||
label="Expected (this month)"
|
||||
value={chargeStats != null ? `${chargeStats.expectedChargeability}%` : "—"}
|
||||
tooltip="Expected chargeability based on all non-cancelled bookings for the current month."
|
||||
sub="All non-cancelled bookings"
|
||||
/>
|
||||
)}
|
||||
<StatCard
|
||||
label="Hours This Month"
|
||||
value={`${Math.round(totalHoursThisMonth)}h`}
|
||||
tooltip="Sum of allocated hours for all active projects in the current calendar month."
|
||||
sub={`${activeProjectIds.size} active project${activeProjectIds.size !== 1 ? "s" : ""}`}
|
||||
/>
|
||||
{canViewScores && resourceWithMeta.valueScore != null && (
|
||||
@@ -316,6 +323,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
|
||||
<StatCard
|
||||
label="Value Score"
|
||||
value={resourceWithMeta.valueScore}
|
||||
tooltip="Composite score (0-100) combining skill depth, breadth, cost efficiency, chargeability, and experience. Hover for breakdown."
|
||||
sub="Price/Quality"
|
||||
/>
|
||||
{resourceWithMeta.valueScoreBreakdown && (
|
||||
@@ -391,7 +399,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
|
||||
{/* Main Skills Badges */}
|
||||
{mainSkills.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5">
|
||||
<h2 className="text-sm font-semibold text-gray-800 mb-3">Main Skills</h2>
|
||||
<h2 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">Main Skills<InfoTooltip content="Up to 2 skills flagged as primary strengths. Used for staffing matching priority." /></h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mainSkills.map((s) => (
|
||||
<span
|
||||
@@ -415,7 +423,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
|
||||
{/* Roles */}
|
||||
{resourceRoles.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5">
|
||||
<h2 className="text-sm font-semibold text-gray-800 mb-3">Roles</h2>
|
||||
<h2 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">Roles<InfoTooltip content="Job functions assigned to this resource. The primary role is used in staffing and timeline displays." /></h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{resourceRoles.map((rr) => (
|
||||
<span
|
||||
@@ -438,7 +446,7 @@ export function ResourceDetail({ resourceId }: ResourceDetailProps) {
|
||||
{/* Skills */}
|
||||
{skills.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5">
|
||||
<h2 className="text-sm font-semibold text-gray-800 mb-3">Skills</h2>
|
||||
<h2 className="text-sm font-semibold text-gray-800 mb-3 flex items-center">Skills<InfoTooltip content="Full skill inventory with proficiency level (1-5) and years of experience. Imported via skill matrix XLSX." /></h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{skills.map((s) => (
|
||||
<span
|
||||
|
||||
Reference in New Issue
Block a user