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:
@@ -0,0 +1,21 @@
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const SystemRolesClient = dynamic(
|
||||
() => import("~/components/admin/SystemRolesClient.js").then((m) => m.SystemRolesClient),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="animate-pulse p-6 space-y-4 max-w-4xl mx-auto">
|
||||
<div className="h-8 w-48 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="space-y-3">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="h-24 w-full bg-gray-200 dark:bg-gray-700 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
export default function SystemRolesPage() {
|
||||
return <SystemRolesClient />;
|
||||
}
|
||||
@@ -1,4 +1,21 @@
|
||||
import { AllocationsClient } from "~/components/allocations/AllocationsClient.js";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const AllocationsClient = dynamic(
|
||||
() => import("~/components/allocations/AllocationsClient.js").then((m) => m.AllocationsClient),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="animate-pulse p-6 space-y-4">
|
||||
<div className="h-8 w-48 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="space-y-2">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<div key={i} className="h-12 w-full bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
export default function AllocationsPage() {
|
||||
return <AllocationsClient />;
|
||||
|
||||
@@ -70,18 +70,18 @@ type EstimateDetail = {
|
||||
};
|
||||
|
||||
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-800 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: EstimateMetric) {
|
||||
@@ -146,7 +146,7 @@ function EstimateDetailPanel({
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Link
|
||||
href={`/estimates/${estimate.id}`}
|
||||
className="inline-flex items-center justify-center rounded-2xl border border-brand-200 bg-brand-50 px-4 py-2 text-sm font-semibold text-brand-700 transition hover:border-brand-300 hover:bg-brand-100"
|
||||
className="inline-flex items-center justify-center rounded-2xl border border-brand-200 dark:border-sky-700 bg-brand-50 dark:bg-sky-950/40 px-4 py-2 text-sm font-semibold text-brand-700 dark:text-sky-300 transition hover:border-brand-300 dark:hover:border-sky-600 hover:bg-brand-100 dark:hover:bg-sky-900/40"
|
||||
>
|
||||
Open workspace
|
||||
</Link>
|
||||
@@ -165,7 +165,7 @@ function EstimateDetailPanel({
|
||||
{latestVersion ? (
|
||||
<>
|
||||
<div className="mt-5 flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Version {latestVersion.versionNumber}
|
||||
{latestVersion.label ? ` - ${latestVersion.label}` : ""}
|
||||
</span>
|
||||
@@ -212,7 +212,7 @@ function EstimateDetailPanel({
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{latestVersion.scopeItems.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-gray-200 px-4 py-3 text-sm text-gray-400">
|
||||
<p className="rounded-2xl border border-dashed border-gray-200 dark:border-gray-700 px-4 py-3 text-sm text-gray-400">
|
||||
No scope rows captured yet.
|
||||
</p>
|
||||
) : (
|
||||
@@ -245,7 +245,7 @@ function EstimateDetailPanel({
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{latestVersion.demandLines.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-gray-200 px-4 py-3 text-sm text-gray-400">
|
||||
<p className="rounded-2xl border border-dashed border-gray-200 dark:border-gray-700 px-4 py-3 text-sm text-gray-400">
|
||||
No staffing demand captured yet.
|
||||
</p>
|
||||
) : (
|
||||
@@ -273,7 +273,7 @@ function EstimateDetailPanel({
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-6 rounded-2xl border border-dashed border-gray-200 px-4 py-6 text-sm text-gray-400">
|
||||
<p className="mt-6 rounded-2xl border border-dashed border-gray-200 dark:border-gray-700 px-4 py-6 text-sm text-gray-400">
|
||||
No versions available for this estimate yet.
|
||||
</p>
|
||||
)}
|
||||
@@ -302,8 +302,8 @@ function EstimateCard({
|
||||
className={clsx(
|
||||
"w-full rounded-3xl border p-5 text-left transition",
|
||||
active
|
||||
? "border-brand-500 bg-brand-50 shadow-sm dark:bg-brand-950/30"
|
||||
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm dark:border-gray-800 dark:bg-gray-950 dark:hover:border-gray-700",
|
||||
? "border-brand-500 bg-brand-50 shadow-sm dark:border-sky-400 dark:bg-sky-950/30"
|
||||
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm dark:border-gray-700 dark:bg-gray-900 dark:hover:border-gray-600",
|
||||
!canInspect && "cursor-default",
|
||||
)}
|
||||
>
|
||||
@@ -319,7 +319,7 @@ function EstimateCard({
|
||||
{estimate.status.replace("_", " ")}
|
||||
</span>
|
||||
{estimate.project && (
|
||||
<span className="rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-600">
|
||||
<span className="rounded-full bg-gray-100 dark:bg-gray-800 px-2.5 py-1 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{estimate.project.shortCode}
|
||||
</span>
|
||||
)}
|
||||
@@ -408,7 +408,7 @@ export function EstimatesClient() {
|
||||
return (
|
||||
<>
|
||||
<div className="app-page space-y-6">
|
||||
<div className="app-surface-strong overflow-hidden bg-gradient-to-br from-white via-white to-brand-50 p-6 dark:from-gray-950 dark:via-gray-950 dark:to-brand-950/40">
|
||||
<div className="app-surface-strong overflow-hidden bg-gradient-to-br from-white via-white to-brand-50 p-6 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900">
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600">
|
||||
|
||||
@@ -1296,7 +1296,7 @@ export function ResourcesClient() {
|
||||
{skills.slice(0, 3).map((s) => (
|
||||
<span
|
||||
key={s.skill}
|
||||
className="inline-block rounded-full bg-brand-50 px-2 py-0.5 text-xs text-brand-700 dark:bg-brand-950/30 dark:text-brand-200"
|
||||
className="inline-block rounded-full bg-brand-50 px-2 py-0.5 text-xs text-brand-700 dark:bg-brand-900/60 dark:text-brand-100"
|
||||
>
|
||||
{s.skill}
|
||||
</span>
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { Suspense } from "react";
|
||||
import { ResourcesClient } from "./ResourcesClient.js";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const ResourcesClient = dynamic(
|
||||
() => import("./ResourcesClient.js").then((m) => m.ResourcesClient),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="animate-pulse p-6 space-y-4">
|
||||
<div className="h-8 w-48 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="space-y-2">
|
||||
{[...Array(10)].map((_, i) => (
|
||||
<div key={i} className="h-12 w-full bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
export default function ResourcesPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<ResourcesClient />
|
||||
</Suspense>
|
||||
);
|
||||
return <ResourcesClient />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import { TimelineView } from "~/components/timeline/TimelineView.js";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const TimelineView = dynamic(
|
||||
() => import("~/components/timeline/TimelineView.js").then((m) => m.TimelineView),
|
||||
{
|
||||
loading: () => (
|
||||
<div className="animate-pulse flex flex-col gap-4 h-full p-6">
|
||||
<div className="h-8 w-48 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
export default function TimelinePage() {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user