refactor(web): decompose TimelineView, ReportBuilder, and ResourceModal into focused components
Extract overlay/popover JSX from TimelineView (1268→1037 lines) into TimelineDragOverlays and TimelinePopovers. Extract ResourceMonthConfigSection from ReportBuilder (1132→1018 lines). Extract ResourceSkillsEditor and ResourceOrgClassification from ResourceModal (1035→714 lines). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
type ReportExplainability,
|
||||
} from "./reportBuilderExplainability.js";
|
||||
import { ReportResultsPanel } from "./ReportResultsPanel.js";
|
||||
import { ResourceMonthConfigSection } from "./ResourceMonthConfigSection.js";
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -753,135 +754,20 @@ export function ReportBuilder() {
|
||||
))}
|
||||
</div>
|
||||
{entity === "resource_month" && (
|
||||
<div className="mt-4 space-y-4 rounded-2xl border border-emerald-200 bg-emerald-50/70 p-4 dark:border-emerald-900/60 dark:bg-emerald-950/20">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-emerald-900 dark:text-emerald-200">
|
||||
Period month
|
||||
</label>
|
||||
<input
|
||||
type="month"
|
||||
value={periodMonth}
|
||||
onChange={(e) => setPeriodMonth(e.target.value)}
|
||||
className="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm text-gray-700 focus:border-emerald-500 focus:ring-emerald-500 dark:border-emerald-900 dark:bg-slate-950 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<p className="max-w-2xl text-sm text-emerald-900/80 dark:text-emerald-200/80">
|
||||
Resource Months uses the CapaKraken holiday and absence logic directly. SAH,
|
||||
booked hours and chargeability are calculated per resource and month with country,
|
||||
state and city context.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-3">
|
||||
{resourceMonthBlueprints.map((blueprint) => (
|
||||
<button
|
||||
key={blueprint.id}
|
||||
type="button"
|
||||
onClick={() => applyBlueprint(blueprint)}
|
||||
className="rounded-2xl border border-emerald-200 bg-white/80 p-4 text-left transition hover:border-emerald-400 hover:bg-white dark:border-emerald-900/70 dark:bg-slate-950/60 dark:hover:border-emerald-700"
|
||||
>
|
||||
<div className="text-sm font-semibold text-emerald-950 dark:text-emerald-100">
|
||||
{blueprint.label}
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-emerald-900/75 dark:text-emerald-200/75">
|
||||
{blueprint.description}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-emerald-200/80 bg-white/60 p-4 dark:border-emerald-900/60 dark:bg-slate-950/40">
|
||||
{displayedResourceMonthCompleteness ? (
|
||||
<div className="mb-4 rounded-2xl border border-emerald-200/80 bg-emerald-50/80 p-4 dark:border-emerald-900/60 dark:bg-emerald-950/20">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={clsx(
|
||||
"rounded-full px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em]",
|
||||
displayedResourceMonthCompleteness.isAuditReady
|
||||
? "bg-emerald-500 text-white"
|
||||
: "bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-200",
|
||||
)}
|
||||
>
|
||||
{displayedResourceMonthCompleteness.isAuditReady
|
||||
? "Audit ready"
|
||||
: "Audit gap"}
|
||||
</span>
|
||||
<span className="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-emerald-900 dark:bg-slate-950 dark:text-emerald-100">
|
||||
{displayedResourceMonthCompleteness.selectedMinimumAuditColumnCount}/
|
||||
{displayedResourceMonthCompleteness.minimumAuditColumnCount} minimum audit
|
||||
columns
|
||||
</span>
|
||||
<span className="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-emerald-900 dark:bg-slate-950 dark:text-emerald-100">
|
||||
{displayedResourceMonthCompleteness.selectedRecommendedColumnCount}/
|
||||
{displayedResourceMonthCompleteness.recommendedColumnCount} recommended
|
||||
columns
|
||||
</span>
|
||||
<span className="rounded-full bg-white px-2.5 py-1 text-[11px] text-emerald-900/80 dark:bg-slate-950 dark:text-emerald-200/80">
|
||||
{selectedTemplate && !hasTemplateDraftChanges
|
||||
? "Saved template status"
|
||||
: "Current builder status"}
|
||||
</span>
|
||||
</div>
|
||||
{displayedResourceMonthCompleteness.missingMinimumAuditColumns.length > 0 ? (
|
||||
<p className="mt-3 text-xs text-amber-800 dark:text-amber-200">
|
||||
Missing audit/export basis columns:{" "}
|
||||
{summarizeMissingColumns(
|
||||
displayedResourceMonthCompleteness.missingMinimumAuditColumns,
|
||||
columnLabelMap,
|
||||
)}
|
||||
</p>
|
||||
) : displayedResourceMonthCompleteness.missingRecommendedColumns.length > 0 ? (
|
||||
<p className="mt-3 text-xs text-emerald-900/80 dark:text-emerald-200/80">
|
||||
Audit-ready, but still missing recommended basis columns:{" "}
|
||||
{summarizeMissingColumns(
|
||||
displayedResourceMonthCompleteness.missingRecommendedColumns,
|
||||
columnLabelMap,
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-3 text-xs text-emerald-900/80 dark:text-emerald-200/80">
|
||||
This view includes the full recommended audit/export basis set for monthly
|
||||
SAH and chargeability checks.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-sm font-medium text-emerald-950 dark:text-emerald-100">
|
||||
Recommended transparency columns
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{RESOURCE_MONTH_RECOMMENDED_COLUMNS.map((column) => (
|
||||
<button
|
||||
key={column}
|
||||
type="button"
|
||||
onClick={() => toggleColumn(column)}
|
||||
className={clsx(
|
||||
"rounded-full border px-3 py-1 text-xs font-medium transition",
|
||||
selectedColumns.has(column)
|
||||
? "border-emerald-500 bg-emerald-500 text-white"
|
||||
: "border-emerald-200 bg-white text-emerald-900 hover:border-emerald-400 dark:border-emerald-900 dark:bg-slate-950 dark:text-emerald-200 dark:hover:border-emerald-700",
|
||||
)}
|
||||
>
|
||||
{columnLabelMap.get(column) ?? column}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-emerald-900/75 dark:text-emerald-200/75">
|
||||
Formula reference: base available hours - holiday deduction - absence deduction =
|
||||
monthly SAH. Chargeability uses booked hours divided by monthly SAH.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-emerald-900/75 dark:text-emerald-200/75">
|
||||
Export recommendation: include both basis columns and computed metrics in the CSV.
|
||||
That keeps Excel as a review layer instead of rebuilding CapaKraken logic outside
|
||||
the product.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-emerald-900/75 dark:text-emerald-200/75">
|
||||
Minimum audit set: month, location context, SAH, holiday deductions, absence
|
||||
deductions, target hours, booked hours and unassigned hours.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ResourceMonthConfigSection
|
||||
periodMonth={periodMonth}
|
||||
onPeriodMonthChange={setPeriodMonth}
|
||||
blueprints={resourceMonthBlueprints}
|
||||
onApplyBlueprint={applyBlueprint}
|
||||
completeness={displayedResourceMonthCompleteness}
|
||||
selectedTemplate={selectedTemplate}
|
||||
hasTemplateDraftChanges={hasTemplateDraftChanges}
|
||||
selectedColumns={selectedColumns}
|
||||
onToggleColumn={toggleColumn}
|
||||
columnLabelMap={columnLabelMap}
|
||||
recommendedColumns={RESOURCE_MONTH_RECOMMENDED_COLUMNS}
|
||||
summarizeMissing={summarizeMissingColumns}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { clsx } from "clsx";
|
||||
|
||||
interface ResourceMonthTemplateCompleteness {
|
||||
scope: "resource_month";
|
||||
isAuditReady: boolean;
|
||||
isRecommendedComplete: boolean;
|
||||
recommendedColumnCount: number;
|
||||
selectedRecommendedColumnCount: number;
|
||||
minimumAuditColumnCount: number;
|
||||
selectedMinimumAuditColumnCount: number;
|
||||
missingRecommendedColumns: string[];
|
||||
missingMinimumAuditColumns: string[];
|
||||
}
|
||||
|
||||
interface ResourceMonthConfigSectionProps<
|
||||
TBlueprint extends { id: string; label: string; description: string },
|
||||
> {
|
||||
periodMonth: string;
|
||||
onPeriodMonthChange: (value: string) => void;
|
||||
blueprints: TBlueprint[];
|
||||
onApplyBlueprint: (blueprint: TBlueprint) => void;
|
||||
completeness: ResourceMonthTemplateCompleteness | null;
|
||||
selectedTemplate: { isShared?: boolean; isOwner?: boolean } | null;
|
||||
hasTemplateDraftChanges: boolean;
|
||||
selectedColumns: Set<string>;
|
||||
onToggleColumn: (column: string) => void;
|
||||
columnLabelMap: Map<string, string>;
|
||||
recommendedColumns: readonly string[];
|
||||
summarizeMissing: (columns: string[], labelMap: Map<string, string>) => string;
|
||||
}
|
||||
|
||||
export function ResourceMonthConfigSection<
|
||||
TBlueprint extends { id: string; label: string; description: string },
|
||||
>({
|
||||
periodMonth,
|
||||
onPeriodMonthChange,
|
||||
blueprints,
|
||||
onApplyBlueprint,
|
||||
completeness,
|
||||
selectedTemplate,
|
||||
hasTemplateDraftChanges,
|
||||
selectedColumns,
|
||||
onToggleColumn,
|
||||
columnLabelMap,
|
||||
recommendedColumns,
|
||||
summarizeMissing,
|
||||
}: ResourceMonthConfigSectionProps<TBlueprint>) {
|
||||
return (
|
||||
<div className="mt-4 space-y-4 rounded-2xl border border-emerald-200 bg-emerald-50/70 p-4 dark:border-emerald-900/60 dark:bg-emerald-950/20">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-emerald-900 dark:text-emerald-200">
|
||||
Period month
|
||||
</label>
|
||||
<input
|
||||
type="month"
|
||||
value={periodMonth}
|
||||
onChange={(e) => onPeriodMonthChange(e.target.value)}
|
||||
className="rounded-xl border border-emerald-300 bg-white px-3 py-2 text-sm text-gray-700 focus:border-emerald-500 focus:ring-emerald-500 dark:border-emerald-900 dark:bg-slate-950 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<p className="max-w-2xl text-sm text-emerald-900/80 dark:text-emerald-200/80">
|
||||
Resource Months uses the CapaKraken holiday and absence logic directly. SAH, booked hours
|
||||
and chargeability are calculated per resource and month with country, state and city
|
||||
context.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-3">
|
||||
{blueprints.map((blueprint) => (
|
||||
<button
|
||||
key={blueprint.id}
|
||||
type="button"
|
||||
onClick={() => onApplyBlueprint(blueprint)}
|
||||
className="rounded-2xl border border-emerald-200 bg-white/80 p-4 text-left transition hover:border-emerald-400 hover:bg-white dark:border-emerald-900/70 dark:bg-slate-950/60 dark:hover:border-emerald-700"
|
||||
>
|
||||
<div className="text-sm font-semibold text-emerald-950 dark:text-emerald-100">
|
||||
{blueprint.label}
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-emerald-900/75 dark:text-emerald-200/75">
|
||||
{blueprint.description}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-emerald-200/80 bg-white/60 p-4 dark:border-emerald-900/60 dark:bg-slate-950/40">
|
||||
{completeness ? (
|
||||
<div className="mb-4 rounded-2xl border border-emerald-200/80 bg-emerald-50/80 p-4 dark:border-emerald-900/60 dark:bg-emerald-950/20">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={clsx(
|
||||
"rounded-full px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em]",
|
||||
completeness.isAuditReady
|
||||
? "bg-emerald-500 text-white"
|
||||
: "bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-200",
|
||||
)}
|
||||
>
|
||||
{completeness.isAuditReady ? "Audit ready" : "Audit gap"}
|
||||
</span>
|
||||
<span className="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-emerald-900 dark:bg-slate-950 dark:text-emerald-100">
|
||||
{completeness.selectedMinimumAuditColumnCount}/
|
||||
{completeness.minimumAuditColumnCount} minimum audit columns
|
||||
</span>
|
||||
<span className="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-emerald-900 dark:bg-slate-950 dark:text-emerald-100">
|
||||
{completeness.selectedRecommendedColumnCount}/{completeness.recommendedColumnCount}{" "}
|
||||
recommended columns
|
||||
</span>
|
||||
<span className="rounded-full bg-white px-2.5 py-1 text-[11px] text-emerald-900/80 dark:bg-slate-950 dark:text-emerald-200/80">
|
||||
{selectedTemplate && !hasTemplateDraftChanges
|
||||
? "Saved template status"
|
||||
: "Current builder status"}
|
||||
</span>
|
||||
</div>
|
||||
{completeness.missingMinimumAuditColumns.length > 0 ? (
|
||||
<p className="mt-3 text-xs text-amber-800 dark:text-amber-200">
|
||||
Missing audit/export basis columns:{" "}
|
||||
{summarizeMissing(completeness.missingMinimumAuditColumns, columnLabelMap)}
|
||||
</p>
|
||||
) : completeness.missingRecommendedColumns.length > 0 ? (
|
||||
<p className="mt-3 text-xs text-emerald-900/80 dark:text-emerald-200/80">
|
||||
Audit-ready, but still missing recommended basis columns:{" "}
|
||||
{summarizeMissing(completeness.missingRecommendedColumns, columnLabelMap)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-3 text-xs text-emerald-900/80 dark:text-emerald-200/80">
|
||||
This view includes the full recommended audit/export basis set for monthly SAH and
|
||||
chargeability checks.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-sm font-medium text-emerald-950 dark:text-emerald-100">
|
||||
Recommended transparency columns
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{recommendedColumns.map((column) => (
|
||||
<button
|
||||
key={column}
|
||||
type="button"
|
||||
onClick={() => onToggleColumn(column)}
|
||||
className={clsx(
|
||||
"rounded-full border px-3 py-1 text-xs font-medium transition",
|
||||
selectedColumns.has(column)
|
||||
? "border-emerald-500 bg-emerald-500 text-white"
|
||||
: "border-emerald-200 bg-white text-emerald-900 hover:border-emerald-400 dark:border-emerald-900 dark:bg-slate-950 dark:text-emerald-200 dark:hover:border-emerald-700",
|
||||
)}
|
||||
>
|
||||
{columnLabelMap.get(column) ?? column}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-emerald-900/75 dark:text-emerald-200/75">
|
||||
Formula reference: base available hours - holiday deduction - absence deduction = monthly
|
||||
SAH. Chargeability uses booked hours divided by monthly SAH.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-emerald-900/75 dark:text-emerald-200/75">
|
||||
Export recommendation: include both basis columns and computed metrics in the CSV. That
|
||||
keeps Excel as a review layer instead of rebuilding CapaKraken logic outside the product.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-emerald-900/75 dark:text-emerald-200/75">
|
||||
Minimum audit set: month, location context, SAH, holiday deductions, absence deductions,
|
||||
target hours, booked hours and unassigned hours.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user