feat: timeline multi-select, demand popover, resource hover card, merged tooltips, dark mode fixes
Major timeline enhancements: - Right-click drag multi-selection with floating action bar (batch delete/assign) - DemandPopover for demand strip details (replaces broken "Loading" modal) - ResourceHoverCard on name hover showing skills, rates, role, chapter - Merged heatmap+vacation tooltips into unified TimelineTooltip component - Fixed overbooking blink animation (date normalization, z-index ordering) - Fixed dark mode sticky column bleed-through in project view - System roles admin page, notification task management, performance review docs Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -11,18 +11,18 @@ import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||||
import { formatDateLong, formatMoney } from "~/lib/format.js";
|
||||
|
||||
const STATUS_STYLES: Record<EstimateStatus, string> = {
|
||||
DRAFT: "bg-slate-100 text-slate-700",
|
||||
IN_REVIEW: "bg-amber-100 text-amber-700",
|
||||
APPROVED: "bg-emerald-100 text-emerald-700",
|
||||
ARCHIVED: "bg-zinc-200 text-zinc-700",
|
||||
DRAFT: "bg-slate-100 text-slate-700 dark:bg-slate-900/30 dark:text-slate-300",
|
||||
IN_REVIEW: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300",
|
||||
APPROVED: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300",
|
||||
ARCHIVED: "bg-zinc-200 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300",
|
||||
};
|
||||
|
||||
const VERSION_STYLES: Record<EstimateVersionStatus, string> = {
|
||||
WORKING: "bg-sky-100 text-sky-700",
|
||||
BASELINE: "bg-violet-100 text-violet-700",
|
||||
SUBMITTED: "bg-amber-100 text-amber-700",
|
||||
APPROVED: "bg-emerald-100 text-emerald-700",
|
||||
SUPERSEDED: "bg-zinc-200 text-zinc-700",
|
||||
WORKING: "bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-300",
|
||||
BASELINE: "bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300",
|
||||
SUBMITTED: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300",
|
||||
APPROVED: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300",
|
||||
SUPERSEDED: "bg-zinc-200 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300",
|
||||
};
|
||||
|
||||
function formatMetricValue(metric: EstimateMetricView) {
|
||||
@@ -43,13 +43,13 @@ export function OverviewTab({ estimate }: { estimate: EstimateWorkspaceView }) {
|
||||
return (
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.1fr),340px]">
|
||||
<section className="space-y-6">
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6 shadow-sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className={clsx("rounded-full px-3 py-1 text-xs font-semibold", STATUS_STYLES[estimate.status])}>
|
||||
{estimate.status.replace("_", " ")}
|
||||
</span>
|
||||
{estimate.project && (
|
||||
<span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-600">
|
||||
<span className="rounded-full bg-gray-100 dark:bg-gray-800 px-3 py-1 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{estimate.project.shortCode}
|
||||
</span>
|
||||
)}
|
||||
@@ -58,43 +58,43 @@ export function OverviewTab({ estimate }: { estimate: EstimateWorkspaceView }) {
|
||||
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-gray-400">Opportunity <InfoTooltip content="External CRM or sales reference linking this estimate to a sales opportunity." /></p>
|
||||
<p className="mt-1 text-sm text-gray-800">{estimate.opportunityId ?? "Not set"}</p>
|
||||
<p className="mt-1 text-sm text-gray-800 dark:text-gray-200">{estimate.opportunityId ?? "Not set"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-gray-400">Base currency <InfoTooltip content="The primary currency for all monetary calculations in this estimate." /></p>
|
||||
<p className="mt-1 text-sm text-gray-800">{estimate.baseCurrency}</p>
|
||||
<p className="mt-1 text-sm text-gray-800 dark:text-gray-200">{estimate.baseCurrency}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-gray-400">Latest version <InfoTooltip content="The most recent version snapshot. Each version captures a full copy of scope, demand, and financials." /></p>
|
||||
<p className="mt-1 text-sm text-gray-800">
|
||||
<p className="mt-1 text-sm text-gray-800 dark:text-gray-200">
|
||||
{latestVersion ? `v${latestVersion.versionNumber}${latestVersion.label ? ` - ${latestVersion.label}` : ""}` : "No version"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-gray-400">Updated</p>
|
||||
<p className="mt-1 text-sm text-gray-800">{formatDateLong(estimate.updatedAt)}</p>
|
||||
<p className="mt-1 text-sm text-gray-800 dark:text-gray-200">{formatDateLong(estimate.updatedAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{latestVersion?.notes && (
|
||||
<div className="mt-5 rounded-2xl border border-gray-100 bg-gray-50 p-4">
|
||||
<div className="mt-5 rounded-2xl border border-gray-100 dark:border-gray-700/50 bg-gray-50 dark:bg-gray-900 p-4">
|
||||
<p className="text-xs uppercase tracking-wide text-gray-400">Version notes</p>
|
||||
<p className="mt-2 text-sm text-gray-700">{latestVersion.notes}</p>
|
||||
<p className="mt-2 text-sm text-gray-700 dark:text-gray-300">{latestVersion.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-gray-900">Scope items <InfoTooltip content="Deliverables or work packages included in this estimate version." /></p>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">Scope items <InfoTooltip content="Deliverables or work packages included in this estimate version." /></p>
|
||||
<span className="text-xs text-gray-400">{latestVersion?.scopeItems.length ?? 0}</span>
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
{(latestVersion?.scopeItems ?? []).slice(0, 4).map((item) => (
|
||||
<div key={item.id} className="rounded-2xl border border-gray-100 px-4 py-3">
|
||||
<div key={item.id} className="rounded-2xl border border-gray-100 dark:border-gray-700/50 px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-medium text-gray-900">{item.name}</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{item.name}</p>
|
||||
<span className="text-xs text-gray-400">{item.scopeType}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,17 +103,17 @@ export function OverviewTab({ estimate }: { estimate: EstimateWorkspaceView }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-gray-900">Demand lines <InfoTooltip content="Staffing demand rows with hours, cost rate, and sell rate per role or resource." /></p>
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">Demand lines <InfoTooltip content="Staffing demand rows with hours, cost rate, and sell rate per role or resource." /></p>
|
||||
<span className="text-xs text-gray-400">{latestVersion?.demandLines.length ?? 0}</span>
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
{(latestVersion?.demandLines ?? []).slice(0, 4).map((line) => (
|
||||
<div key={line.id} className="rounded-2xl border border-gray-100 px-4 py-3">
|
||||
<div key={line.id} className="rounded-2xl border border-gray-100 dark:border-gray-700/50 px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-medium text-gray-900">{line.name}</p>
|
||||
<span className="text-xs text-gray-500">{line.hours.toFixed(1)} h</span>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{line.name}</p>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">{line.hours.toFixed(1)} h</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -124,44 +124,44 @@ export function OverviewTab({ estimate }: { estimate: EstimateWorkspaceView }) {
|
||||
</section>
|
||||
|
||||
<aside className="space-y-4">
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<p className="text-sm font-semibold text-gray-900">Summary metrics <InfoTooltip content="Key financial indicators derived from the latest version's demand lines." /></p>
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 shadow-sm">
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">Summary metrics <InfoTooltip content="Key financial indicators derived from the latest version's demand lines." /></p>
|
||||
<div className="mt-4 space-y-3">
|
||||
{latestMetrics.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No derived metrics available yet.</p>
|
||||
) : (
|
||||
latestMetrics.map((metric) => (
|
||||
<div key={metric.id} className="flex items-center justify-between gap-3 rounded-2xl bg-gray-50 px-4 py-3">
|
||||
<div key={metric.id} className="flex items-center justify-between gap-3 rounded-2xl bg-gray-50 dark:bg-gray-900 px-4 py-3">
|
||||
<span className="text-xs uppercase tracking-wide text-gray-400">{metric.label}</span>
|
||||
<span className="text-sm font-semibold text-gray-900">{formatMetricValue(metric)}</span>
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">{formatMetricValue(metric)}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<p className="text-sm font-semibold text-gray-900">Version context <InfoTooltip content="Metadata about the latest version, including its workflow status and linked records." /></p>
|
||||
<div className="rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 shadow-sm">
|
||||
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100">Version context <InfoTooltip content="Metadata about the latest version, including its workflow status and linked records." /></p>
|
||||
<div className="mt-4 space-y-3">
|
||||
{latestVersion ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm text-gray-500">Status</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">Status</span>
|
||||
<span className={clsx("rounded-full px-2.5 py-1 text-xs font-medium", VERSION_STYLES[latestVersion.status])}>
|
||||
{latestVersion.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm text-gray-500">Assumptions</span>
|
||||
<span className="text-sm font-medium text-gray-900">{latestVersion.assumptions.length}</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">Assumptions</span>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{latestVersion.assumptions.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm text-gray-500">Snapshots</span>
|
||||
<span className="text-sm font-medium text-gray-900">{latestVersion.resourceSnapshots.length}</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">Snapshots</span>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{latestVersion.resourceSnapshots.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm text-gray-500">Exports</span>
|
||||
<span className="text-sm font-medium text-gray-900">{latestVersion.exports.length}</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">Exports</span>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{latestVersion.exports.length}</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user